Auth.js에서 Naver OAuth 사용 시 발생하는 에러 해결하기

no-pla·2024년 9월 5일
0

트러블 슈팅

목록 보기
7/7
post-thumbnail

Auth.js는 OAuth의 표준 스펙을 엄격하게 준수한다. 그렇기에 네이버 로그인의 경우, 특정 값이 스펙에 맞지 않게 반환하기 때문에 에러가 발생했다.

에러 로그

Server error
There is a problem with the server configuration.

Check the server logs for more information.

[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: OperationProcessingError: "response" body "expires_in" property must be a positive number

이는 expires_in이 정수 형식(number)이어야 하지만, 문자열 형식으로 반환하기 때문에 발생한 에러였다. 이전 버전(v4)에서는 이러한 에러가 발생하지 않았지만, Next-Auth가 v5로 버전을 업그레이드하면서 유사한 문제가 많은 OAuth(인스타그램, OneLogin, Foursquare)에서 대거 발생하고 있다.

이러한 문제점을 찾은 다른 개발자들이 이미 이 문제를 제보했지만, 네이버 측에서는 사이드 이펙트의 우려로 해당 사항을 수정하는 것은 어렵다고 한다.

관련 Next-Auth discussion

이러한 문제를 해결한 예시 중 하나로, 인스타그램의 표준 스펙을 따르지 않아 발생하는 문제를 해결했던 인터셉터를 참고하여 네이버 OAuth를 사용할 수 있도록 인터셉터를 작성해 보자.

문제 상황

  1. oauth4webapi는 Next-Auth v5의 반환 값이 표준 스펙을 준수하는가를 엄격하게 판단함.
  2. 네이버 로그인 사용 시 반환되는 expires_in 값이 공식 문서에 기재된 것과는 달리 정수 값이 아닌 문자열임.

네이버 로그인의 oauth4webapi에서 에러가 발생한 부분은 이 부분이다.

해결 방법

인스타그램의 인터셉터를 참고하여 네이버 인터셉터를 작성해 보았다.

인스타그램의 인터셉터는 반환값을 수정해 스펙에 맞도록 수정하는 방식으로 동작한다. 위의 예시를 참고하여 반환값을 인터셉팅해서 expires_in을 number로 변환하여 수정해 표준 스펙을 준수하도록 수정을 할 것이다.

  1. 먼저 fetch 함수를 수정하여, 네이버 로그인 시에는 인터셉터를 적용하고, 다시 복원할 수 있도록 한다.
// app/api/auth/[...nextauth]/route.ts
import { GET as AuthGET, POST as AuthPOST } from "@/auth";
import { naverFetchInterceptor } from "@/interceptor/naver-interceptor";
import { type NextRequest } from "next/server";

const originalFetch = fetch;

export async function POST(req: NextRequest) {
  return await AuthPOST(req);
}

export async function GET(req: NextRequest) {
  const url = new URL(req.url);

  if (url.pathname === "/api/auth/callback/naver") {
    global.fetch = naverFetchInterceptor(originalFetch);
    const response = await AuthGET(req);
    global.fetch = originalFetch;
    return response;
  }
  return await AuthGET(req);
}

다음은 네이버 로그인 시, 실행되는 인터셉터로 옳지 않은 스펙(expires_in)을 올바른 형식으로 수정해 준다.

// interceptor/naver-interceptor.ts
/**
 * This interceptor is used to modify the response of the naver access token request as it does not strictly follow the OAuth2 spec
 * 네이버 아이디 로그인의 `expires_in`이 정수 타입이 아닌 문자열 타입으로 반환됩니다.
 * @param originalFetch
 */
export const naverFetchInterceptor =
  (originalFetch: typeof fetch) =>
  async (
    url: Parameters<typeof fetch>[0],
    options: Parameters<typeof fetch>[1] = {}
  ) => {
    if (
      url === "https://nid.naver.com/oauth2.0/token" &&
      options.method === "POST"
    ) {
      const response = await originalFetch(url, options);
      const clonedResponse = response.clone();
      const body = await clonedResponse.json();

      body.expires_in = Number(body.expires_in); // 문자열로 되어 있는, expires_in을 숫자로 변환

      const modifiedResponse = new Response(JSON.stringify(body), {
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
      });

      return Object.defineProperty(modifiedResponse, "url", {
        value: url,
      });
    }

    return originalFetch(url, options);
  };

아래는 auth.ts 파일의 구성이다.

// auth.ts
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import Naver from "next-auth/providers/naver";
import { prisma } from "./prisma";
import { Provider } from "next-auth/providers";

export const CustomNaverAuthProvider: Provider = Naver({
  clientId: process.env.AUTH_NAVER_ID,
  clientSecret: process.env.AUTH_NAVER_SECRET,
  authorization: "https://nid.naver.com/oauth2.0/authorize?response_type=code",
});

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [CustomNaverAuthProvider],
});

export const { GET, POST } = handlers;

결과

로그인 창도 제대로 뜨고, 로그인과 DB까지 제대로 동작한다!

  • 로그인 창
  • 로그인 유저 정보
  • DB (Provider 네이버로 잘 저장된다.)

Auth.js(Next-Auth v5)가 업데이트되면서 표준 스펙을 지키지 않는 OAuth를 사용하지 못하는 에러가 종종 발생하는 것 같다. 이러한 방식으로 특정 OAuth 실행 시에만 인터셉터를 작성하여 표준 스펙을 지킬 수 있도록 수정해야겠다.

0개의 댓글