이전 포스팅에서 next-auth 라이브러리를 사용하게 된 이유를 성찰? 해보았는데, 어떻게 사용하는지를 한 번 알아보고자 한다.
page router에서는 pages/api/auth/[...nextauth].ts
dynamic router파일을 만들어야 한다. (OAuth
클라이언트단에서 클라이언트 서버단을 매개체로 서버와 통신한 값을 받아와 클라이언트로 인증/인가 통신의 response를 보내준다.
먼저 매개체 역할을 할 파일에 auth-option을 만들어 준다.
next-auth를 초기화(셋팅) 할 때 설정해 주며, [...nextauth].ts
로 요청 보내면, auth-option안에 본인이 설정한 코드로 로직이 실행된다.
//pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
export const authOptions = {
//... 생략
// page custom
pages: {
signIn: "/auth-nextAuth",//localhost:3000/auth-nextAuth
}
//ex
const hasCustomPage =authOptions.pages.signIn : undefind|path
if(!hasCustomPage) return api/auth/signIn
if(hasCustomPage) return hasCustomPage
가장 먼저 알아야 할 것은 custom page라 생각한다.
next-auth에서는 default값으로 ( 라이브러리 자체 )
api/auth/signIn
api/auth/signout
api/auth/signIn
으로 url 접속을 한다면 option의 pages에서 설정한 주소로 callback하게 된다.
provider에는 "각" 로그인에 사용할 인증 공급자 배열을 넣을 수 있다.
인증 공급자라 함은 내가 현재 사용할 Credential
과 facebook
, naver
와 같은 OAuth공급자를 담아놓는 배열 통이다.
아래의 코드는 CredentailsProvider로 로그인 하기 위한 로직이다.
//pages/api/auth/[...nextauth].ts
export const authOptions = {
// Configure one or more authentication providers
providers: [
CredentialsProvider({
id: "HTTLogin", //고유 provider의 id
type: "credentials",
name: "CredentialsLogin",
credentials: { // next-auth
email: { type: "text" },
password: { type: "password" },
},
}),
// ...add more providers here
FaceBookProvider({...}),
GithubProvider({...}),
],
}
export default NextAuth(authOptions)
사진에서 보면 CredentailsProvider
에 들어가는 객체의 property를 나타낸 것이다.
credentials에는 input의 프로퍼티가 들어간다.
이 input의 property를 생성한 만큼 default로 next-auth에서 제공하는 /api/auth/signIn
이라는 페이지에서 Input이 본인이 짜놓은 input의 종류와 프로퍼티로 생성되는데, 생성된 Input은 signIn page에서 확인이 가능하다.
그렇지만 우리는 customPage(내가 만들어놓은 페이지)에서 value를 next-auth에 보낼 것이므로 어떻게 하면 좋을지 생각이 들게 된다.
credentails라는 값은 반드시 필수로 넣어달라고 하는데 어떻하면 좋단 말인가??
당연히 credentials에 들어갈 field를 적어놓아도 무방하겠지만 그건 올바른 코드는 아니라 생각한다.
고민끝에
credentails: { email: { type: "text" }, password: { type: "password" }} // 댕댕냥 프로젝트
credenatils : { } // 다음번 프로젝트에서는 객체만 명시
프로젝트에서는 타입만 명시해줬지만, 다음에는 그냥 객체 상태로 두어도 무방하다 생각해 객체로 두는 쪽으로 하기로 결정하였다.
비동기 함수이고 클라이언트에서 값을 보내면 그 값을 가지고 인증을 확인 한다.
// pages/signIn
import { signIn } from "next-auth/react";
const handleSubmit = async (value: Schema) => {
const result = await signIn("HTTLogin", {
...value,
redirect: false,
callbackUrl: "/",
});
console.log(result);
};
next-auth에서 제공하는 메소드안에 위에서 id
값을 명시해주고 값을 보내주면 id값과 일치하는 Provider의 authorize 메소드로 값이 보내진다.
보내진 값을 가지고 처리하면 된다.
async authorize(credentials, req) {
const { email, password } = credentials as IAuth;
try {
const res = await axios.post<IAuth, AxiosResponse>(// AxiosResponse를 명시해줘야 eslint에 안걸린다.
"http://localhost:4000/users",
{
email,
password,
},
);
return res.data;
} catch (err) {
if (err instanceof AxiosError) {
throw new Error(JSON.stringify(err.response?.data));
}
}
},
위의 로직에서 중요한 점은 catch
문인데
signIn
메소드의 response타입에서 에러는 null값이다. 따라서 customError message를 띄워주려면 json으로 error를 내보내줘야 한다.
// client method signIn의 response type
interface SignInResponse {
error: string | null
status: number
ok: boolean
url: string | null
}
// authOptions
async authorize(...) {
catch (err) {
if (err instanceof AxiosError) {
throw new Error(JSON.stringify(err.response?.data));
}
// 정의가 되어있어서 이메일, 비번에 따른 에러 분기 처리를 해주려면 stringfy로 에러를 던줘줘야함 (status는 항상 401이다. )
// null을 return 하면 error를 내보낸다.따라서 커스터마이징 하고 싶으면 위와 같이 throw new Error 하고,
// 기본 내장 error를 내보내고 싶으면 return null 하자
return null
// pages/signIn
const handle = (...) => {
const res = await signIn('HTTPLogin',{ ... } )
if(!res?.ok) throw new Error(JSON.parser(res))
return res;
}
session객체는 기본적으로 전략을 jwt
로 할건지 database
로 할건지로 나눌 수 있으며
기본적으로 jwt
로 설정되어있다.
뿐만아니라 next-auth로 인증 후 sessionToken값을 custom 할수가 있다.
session: {
// Choose how you want to save the user session.
// The default is `"jwt"`, an encrypted JWT (JWE) stored in the session cookie.
// If you use an `adapter` however, we default it to `"database"` instead.
// You can still force a JWT session by explicitly defining `"jwt"`.
// When using `"database"`, the session cookie will only contain a `sessionToken` value,
// which is used to look up the session in the database.
strategy: "database",
// Seconds - How long until an idle session expires and is no longer valid.
maxAge: 30 * 24 * 60 * 60, // 30 days
// Seconds - Throttle how frequently to write to database to extend a session.
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
// The session token is usually either a random UUID or string, however if you
// need a more customized session token string, you can define your own generate function.
generateSessionToken: () => {
return randomUUID?.() ?? randomBytes(32).toString("hex")
}
}
이렇게 문구들이 있는데, 기본적으로 jwt로 되어있기 때문에 프로젝트에서는 건드리지 않았다.
그렇지만, maxAge 값을 다뤄야 하기 때문에 요번 블로그를 통해 다뤄보고자 한다.
callbacks객체를 알아보기 전, 인자값들을 커스텀 할 필요가 있다.
우리 프로젝트에서 넘어오는 user의 값을 next-auth와 공유해야 하기 때문이다.
아래 작성한 custom type은 내가 사용하는 프로젝트에서 사용했던 타입 declare다.
// src/types/auth/next-auth.d.ts
import 'next-auth';
/**
* @type DefaultUser : export interface DefaultUser {
id: string
name?: string | null
email?: string | null
image?: string | null
}
* @type User : export interface User extends DefaultUser {} // 일부로 빈 객체
*/
declare module 'next-auth' {
// 여기서 재 정의한 타입이 callbacks의 user의 타입으로 정의됨
interface User {
accessToken: string;
refreshToken: string;
}
// 여기서 재정의한 타입이 session의 타입으로 재정의 됨
interface Session {
user: {
accessToken: string;
refreshToken: string;
};
}
}
/**
* JWT는 next-auth의 subModule입니다.
* https://next-auth.js.org/getting-started/typescript#submodules
*/
declare module 'next-auth/jwt' {
// 여기서 재정의한 JWT는 callbacks의 jwt의 인자 값인 token의 type을 재정의 하여 타입추론이 되게끔합니다.
interface JWT {
accessToken: string;
refreshToken: string;
role: string;
}
}
authOptions의 session에서 strategy : 'JWT'
로 설정 했다면 JWT를 내부를 조작할 수 있다.
@type Jwt(params: {
token: JWT
user: User | AdapterUser
account: A | null
profile?: P
trigger?: "signIn" | "signUp" | "update" // update일 때 토큰을 갈아껴주기
isNewUser?: boolean
session?: any
}
) => Awaitable<JWT>
async jwt:Jwt({ user, token,session,...rest }) {
if (user) {
token.role = "user";
token.accessToken = user.token;
}
return token;
},
jwt에 있는 인자값들은 총 6개이지만, 간단하게 유저가 처음 로그인 했을 때를 가정해서 필요한 것만을 설명할 것이다.
user
: user는 authorize
에서 반환 한 값이다. 또는 adapterUser의 값이기도 하다.
token
: next-auth의 helper함수인 getToken으로 추출 할 수 있는 토큰을 이 jwt 메소드에서 관리하는데 token을 반환하면 getToken
에 들어가게 된다.
session
: 내가 정의한 session이다. 이 session은 callbacks - session에서 다룬다.
이 토큰 값을 확인해보면
{
email: 'test@test.com',
sub: '018e', // 이건 내가 보내준 id라 신경 ㄴㄴ
role: 'user',
accessToken: 'eyJhbGciOiJIUzI1NiJ9.dGVzdEB0ZXN0LmNvbQ.pEMrDeN-FOYXN4JPzwh5BZpoPUldVTL4bVEe6CsimII',
exp: 1724882235, // 2024년 8월 28일 21시 57분 15초 (UTC)
iat: 1722290235, // 2024년 7월 29일 21시 57분 15초 (UTC)
jti: 'af826c41-1d4f-423b-a13a-30cf4542a208'
}
이렇게 내가 추가한 토큰
과 role
그리고 자동?으로 email과 다른 것들이 추가되어있는 것이 보이는데
JWT의 token은 위와 같이 자동 default를 가지게 되는데, next-auth에서 제공하는 JWT의 토큰 만료는 30일임을 알자
따라서 토큰의 만료 기간을 다시 설정 하고 싶다면, next-auth.d.ts
에서 정의, 또는 삽입해주자
token.exp='하루'
session은 내가 getSession
, useSession
등과 같이 세션여부를 확인하고 다루기 위해 있는 콜백함수이다.
@ types export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
}
expires: ISODateString
}
async session({ session, token, trigger }) {
session.user.accessToken = token.accessToken;
session.user.refreshToken = token.refreshToken;
return session;
},
JWT에서 설명한 것과 동일하지만,
trigger
: 이 인자는 ' update ' 만을 가지고 있는데, 우리가 설계한 세션기간을 늘리거나 토큰을 새로 발급 받았을 때 사용 할 로직이다. 그리고 session을 확인해보면
{
user: { name: undefined, email: 'test@test.com', image: undefined },
expires: '2024-08-28T21:54:14.755Z'
}
이렇게 따로 expires
가 정의되어있는데, 이는 session의 유효기간을 나타낸다.
credential의 authorize에서 값을 반환 후 callbacks의 session과 jwt 둘 중 무엇이 먼저 실행될지 궁금했는데
공식문서상에서 정의 내려주고 있다.
jwt 토큰 전략을 사용하면, 우선적으로 jwt()
실행 후 -> session()
이 실행된다.
살짝 헷갈릴뻔 한 것이
token과 session의 유효기간인데,
따라서 서버에서 token을 5일기간이라 설정하면
session에서 expires를 설정 할 수 있는 방법은 2가지 인데,
하나는 authOptions의 session의 maxAge
에서 정의 해주면 된다.
다른 하나는 직접 session()
에서 expires를 정의 해주면 된다.
session:{ maxAge : 5 * 24 * 60 * 60 } // 이방법
callbacks:{
async jwt({token}{
const expiresIn = 5 * 24 * 60 * 60// 5일
const expireationDate = Math.floor(Date.now() / 1000) + expiresIn // 현재 + expiresIn을 해야 기간설정임
token.exp = expireationDate
},
async session({session}){
session.expires = new Date(token.exp * 1000).toISOString() // expiresType이 ISODateString
}
}
next-auth라이브러리를 공부하고 적용해 나가면서,
이번 프로젝트에서 auth관련이 제일 섬세하고, 숙련된 지식을 요구한다고 생각이든다.
하면서 모르는 부분을 정리하고, 알아가면서 아직까지도, 제대로된 auth처리가 부족하다는 느낌을 많이 받기에 prisma를 통해서 한번 따로 풀스택으로 직접 구현과 정리해 나가야겠다.