Options
next-auth를 사용하여 jwt를 사용하려면 NextAuthOptions 사용하여야 한다. OAuth를 구현하면서 기존에는 GoogleProvider만 사용하였던 Option을 좀 더 사용해보자.
Option은 API 경로에서 초기화할 때 NextAuth.js에 전달된다.
https://next-auth.js.org/configuration/options
사용자가 로그인을 완료 하면 Authorization Server가 파라미터의 redirect_url로 authorization code를 포함하여 리다이렉팅 시키며 NextAuth는 client_id, client_secret, redirect_url 및 받아온 authorization code 값을 파라미터로하여 AccessToken을 포함한 여러 계정정보를 받아온다.
이때 NextAuth에서 관리하는 Model중 하나인 User Model에 OAuth Profile (id, name, email, image) 정보가 저장되며 이는 Provider의 Profile 콜백을 통하여 확장할 수 있다. 또한 Account Model에는 OAuth 계정에 관한 정보가 저장되며 일반적으로 리소스 서버에 요청을할때 필요한 AccessToken도 이에 포함된다.
그런다음 NextAuth 옵션 객체의 callbacks의 메소드들이 실행되며 이때 클라이언트 사이드 쪽에서 useSession을 통해 토큰의 정보 및 유저 정보를 가져가게 할 수 있게 하기 위해서 초기로그인시 필요한 정보를 담아 NextAuth 내의 jwt 토큰을 만들어주고 세션에도 필요한 정보를 넘길 수 있다.
주의할점은 jwt메서드의 token은 Authorization Server에서 받아온 토큰을 뜻하는것이 아니라는것이다. jwt메서드의 token은 NextAuth의 Account Models과 User Models를 정보를 조합하여 NextAuth에서 유지하는 토큰이다. Models에 관한 정보는 NextAuth 공식문서에서 볼 수 있다.
jwt calback
웹 토큰이 실행 또는 업데이트될때마다 콜백이 실행된다.반환된 값은 암호화 되어 쿠키에 저장된다.
import { NextAuthOptions, User } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const options: NextAuthOptions = {
providers: [ ... ],
callbacks: {
async jwt({ token, user, account}) {
// 초기 로그인시 User 정보를 가공하여 반환
if (account && user) {
return {
accessToken: account.access_token,
accessTokenExpires: account.expires_at,
refreshToken: account.refresh_token,
user,
};
}
return token;
},
},
};
session callback
클라이언트 사이드에서 useSession을 사용했을때 확인할 수 있는 session 값에 대한 콜백이다.
typescript에서 이를 확장하기 위해선 최상위 디렉토리에서 next_auth.d.ts파일을 만들어 기본 세션 interface를 확장하여 사용할 수 있다.
import { NextAuthOptions, User } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const options: NextAuthOptions = {
providers: [ ... ],
callbacks: {
...
async session({ session, token }) {
session.user = token.user as User;
session.accessToken = token.accessToken as string;
session.accessTokenExpires = token.accessTokenExpires as string;
session.error = token.error;
return session;
},
},
};
next_auth.d.ts
import { DefaultSession } from "next-auth";
import { DefaultJWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session extends DefaultSession {
accessToken?: string;
accessTokenExpires?: number;
error?: any;
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
accessToken?: string;
accessTokenExpires?: number;
refreshToken?: string;
error?: any;
}
}
next_auth에서 access_token을 refresh 하기
https://authjs.dev/guides/basics/refresh-token-rotation
next_auth 공식 문서에서 친절하게 access_token을 refresh 하는 샘플 코드를 제공해준다. 하지만 이 방법은 access_token이 만료된 후에 권한이 끊기고 난 후에 refresh 하는 방법이다. 좀 더 매끄러운 권한 연장을 위해서 참고한 블로그 글과 공식문서를 합쳐서 아래와 같이 구성했다.
import axios from "axios";
import { type NextAuthOptions, type TokenSet, type User } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const options: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET_KEY || "",
}),
],
session: { strategy: "jwt" },
callbacks: {
jwt: async ({ token, account }) => {
// 초기 로그인시 User 정보를 가공하여 반환
if (account) {
return {
accessToken: account.access_token,
accessTokenExpires: account.expires_at,
refreshToken: account.refresh_token,
};
}
const nowTime = Math.round(Date.now() / 1000);
const shouldRefreshTime = token.accessTokenExpires - 10 * 60 - nowTime;
// 토큰이 만료되지 않았을 때, 기존 토큰 반환
if (shouldRefreshTime > 0) {
return token;
}
return refreshAccessToken(token);
},
async session({ session, token }) {
session.user = token.user as User;
session.accessToken = token.accessToken as string;
session.accessTokenExpires = token.accessTokenExpires as number;
session.error = token.error;
return session;
},
},
};
const refreshAccessToken = async (token: any) => {
try {
const url = "https://oauth2.googleapis.com/token";
const params = {
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET_KEY,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
};
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
const res = await axios.post(url, {
headers,
params,
});
const refreshedTokens: TokenSet = await res.data;
if (res.status !== 200) {
throw refreshedTokens;
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires:
Math.round(Date.now() / 1000) + (refreshedTokens.expires_in as number),
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
};
} catch (error) {
console.log(error);
return {
...token,
error,
};
}
};
NextJS 13에 대한 정보가 너무 적다 .... 대부분의 자료가 13 이전버전이고, next_auth 공식문서에서도 13버전에 대한 자료는 거의 없다고 봐도 무방하다 ...