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)에서 대거 발생하고 있다.
이러한 문제점을 찾은 다른 개발자들이 이미 이 문제를 제보했지만, 네이버 측에서는 사이드 이펙트의 우려로 해당 사항을 수정하는 것은 어렵다고 한다.
이러한 문제를 해결한 예시 중 하나로, 인스타그램의 표준 스펙을 따르지 않아 발생하는 문제를 해결했던 인터셉터를 참고하여 네이버 OAuth를 사용할 수 있도록 인터셉터를 작성해 보자.
oauth4webapi
는 Next-Auth v5의 반환 값이 표준 스펙을 준수하는가를 엄격하게 판단함.expires_in
값이 공식 문서에 기재된 것과는 달리 정수 값이 아닌 문자열임.네이버 로그인의 oauth4webapi에서 에러가 발생한 부분은 이 부분이다.
인스타그램의 인터셉터를 참고하여 네이버 인터셉터를 작성해 보았다.
인스타그램의 인터셉터는 반환값을 수정해 스펙에 맞도록 수정하는 방식으로 동작한다. 위의 예시를 참고하여 반환값을 인터셉팅해서 expires_in
을 number로 변환하여 수정해 표준 스펙을 준수하도록 수정을 할 것이다.
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까지 제대로 동작한다!
Auth.js(Next-Auth v5)가 업데이트되면서 표준 스펙을 지키지 않는 OAuth를 사용하지 못하는 에러가 종종 발생하는 것 같다. 이러한 방식으로 특정 OAuth 실행 시에만 인터셉터를 작성하여 표준 스펙을 지킬 수 있도록 수정해야겠다.