[ 공모전 ] Auth : 토큰과 전역관리(authOptions of next-auth )

최문길·2024년 7월 30일
0

공모전

목록 보기
35/46

이전 포스팅에서 next-auth 라이브러리를 사용하게 된 이유를 성찰? 해보았는데, 어떻게 사용하는지를 한 번 알아보고자 한다.


page router에서는 pages/api/auth/[...nextauth].ts
dynamic router파일을 만들어야 한다. (OAuth

클라이언트단에서 클라이언트 서버단을 매개체로 서버와 통신한 값을 받아와 클라이언트로 인증/인가 통신의 response를 보내준다.

먼저 매개체 역할을 할 파일에 auth-option을 만들어 준다.

auth-option

next-auth를 초기화(셋팅) 할 때 설정해 주며, [...nextauth].ts 로 요청 보내면, auth-option안에 본인이 설정한 코드로 로직이 실행된다.

authOptions - pages

//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
    등으로 하기때문에 authOptions의 pages에서
    default로 생성된 로그인 page를 overriding 할 수 있다.
    참고 Docs

api/auth/signIn으로 url 접속을 한다면 option의 pages에서 설정한 주소로 callback하게 된다.

authOptions - CredentialsProvider

provider에는 "각" 로그인에 사용할 인증 공급자 배열을 넣을 수 있다.
인증 공급자라 함은 내가 현재 사용할 Credentialfacebook , 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 : { } // 다음번 프로젝트에서는 객체만 명시

프로젝트에서는 타입만 명시해줬지만, 다음에는 그냥 객체 상태로 두어도 무방하다 생각해 객체로 두는 쪽으로 하기로 결정하였다.

CredentialsProvider - authorize 메소드(함수)

비동기 함수이고 클라이언트에서 값을 보내면 그 값을 가지고 인증을 확인 한다.

// 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;
  }

authOptions - session

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 값을 다뤄야 하기 때문에 요번 블로그를 통해 다뤄보고자 한다.

authOptions - callbacks

callbacks의 인자값 타입 커스텀하기

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;
  }
}

callbacks - jwt

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과 다른 것들이 추가되어있는 것이 보이는데

  • iat : 토큰이 생성된 날이다
  • exp : 토큰이 만료되는 날이다.
  • jti : 토큰의 고유 아이디

JWT의 token은 위와 같이 자동 default를 가지게 되는데, next-auth에서 제공하는 JWT의 토큰 만료는 30일임을 알자
따라서 토큰의 만료 기간을 다시 설정 하고 싶다면, next-auth.d.ts 에서 정의, 또는 삽입해주자

token.exp='하루' 

callbacks - session

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의 유효기간을 나타낸다.

session() vs jwt(), session expire vs jwt expire

실행 순서

credential의 authorize에서 값을 반환 후 callbacks의 session과 jwt 둘 중 무엇이 먼저 실행될지 궁금했는데
공식문서상에서 정의 내려주고 있다.

jwt 토큰 전략을 사용하면, 우선적으로 jwt() 실행 후 -> session() 이 실행된다.

expire

살짝 헷갈릴뻔 한 것이
token과 session의 유효기간인데,

  • token은 인가의 유효기간이며,
  • session은 인증의 유효기간, 즉 서버와의 유지 기간을 나타냄을 명심하자

따라서 서버에서 token을 5일기간이라 설정하면

  • token도 5일 , session 유지기간도 session-cookie와 같은 경우가 아니기에, 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를 통해서 한번 따로 풀스택으로 직접 구현과 정리해 나가야겠다.

0개의 댓글

관련 채용 정보