Loading...
본문 바로가기
👥
총 방문자
📖
0개 이상
총 포스팅
🧑
오늘 방문자 수
📅
0일째
블로그 운영

여러분의 방문을 환영해요! 🎉

다양한 개발 지식을 쉽고 재미있게 알려드리는 블로그가 될게요. 함께 성장해요! 😊

코딩 정보/NextJs

[개념부터 nextjs] next-auth 사용하기

by 꽁이꽁설꽁돌 2025. 8. 14.
728x90
반응형
     

목차

     

     

    next-auth를 사용하면서 차근차근 고민했던 부분을 정리하고자 한다.

     

     

    next-auth 도입 이유

    • csrf 토큰을 통한 보안 증가

    이때 Next-Auth의 CSRF는 “Next-Auth 자신”과 브라우저 사이를 보호하는 것입니다.

    브라우저와 백엔드 사이를 보호하는 것이 아니기 때문에 그건 따로 설정해 주어야 합니다.

    • http-only 설정을 통한 xss 공격 보호
    • 추후 소셜 로그인 확장성에 대한 유연함

     

    ++추가

    1. 루시아, better-auth등은 db 구축 기반의 인증 방식 사용 

    2. next-auth의 자체 쿠키에 암호화 방식으로 안전성 보장

     

     

     

     

    간략한 사용 방법

    이번 글은 커스텀 로그인 방식을 사용할 것이기 때문에  CredentialProvider를 중점적으로 보면 될 것 같다.

     

     

    전체 코드

    import NextAuth, { Session } from 'next-auth';
    import CredentialsProvider from 'next-auth/providers/credentials';
    import KakaoProvider from 'next-auth/providers/kakao';
    import getTokenExpiration from './shared/lib/getTokenExpiration';
    import serverApi from './shared/api/server-api';
    import {
      LoginSuccessResponse,
      RoleResponse,
    } from './features/login/model/type';
    import UserInfoType from './entities/my/model/type';
    
    // TODO: 추후 루시아로 변경
    export const { auth, handlers, signIn, signOut } = NextAuth({
      secret: process.env.NEXT_AUTH_SECRET as string,
      trustHost: true,
      pages: {
        error: '/login?callbackUrl=/',
      },
      providers: [
        KakaoProvider({
          clientId: process.env.KAKAO_CLIENT_ID as string,
          clientSecret: process.env.KAKAO_SECRET_KEY as string,
        }),
        CredentialsProvider({
          name: 'credentials',
          credentials: {
            studentId: { label: '학번', type: 'text' },
            password: { label: '비밀번호', type: 'password' },
          },
    
          async authorize(credentials): Promise<any> {
            try {
              const response = await serverApi.post('users/auth/login', {
                json: credentials,
              });
              const data: LoginSuccessResponse = await response.json();
              const userResponse = await serverApi.get('users', {
                headers: {
                  Authorization: `Bearer ${data.data.accessToken}`,
                },
              });
              const userData: { data: { user: UserInfoType } } =
                await userResponse.json();
              return {
                accessToken: data.data.accessToken,
                refreshToken: data.data.refreshToken,
                user: userData.data.user,
              };
            } catch (error) {
              console.error('[authorize error]', error);
              return null;
            }
          },
        }),
      ],
      callbacks: {
        jwt: async ({ token, account, user }) => {
          // 소셜 로그인
          if (account?.provider === 'kakao') {
            return {
              ...token,
              accessToken: account.access_token,
              refreshToken: account.refresh_token,
            };
          }
    
          if (user && account?.provider === 'credentials') {
            const expiredTime = getTokenExpiration(user.accessToken as string);
    
            try {
              const headers = {
                Authorization: `Bearer ${user.accessToken}`,
              };
              const rolesRes = await serverApi.get('users/roles', { headers });
              const rolesData: RoleResponse = await rolesRes.json();
              console.log('token', token);
    
              return {
                ...token,
                accessToken: user.accessToken,
                refreshToken: user.refreshToken,
                expiresAt: expiredTime,
                user: user.user,
                role: rolesData.data.role,
              };
            } catch (error) {
              console.error('[role fetch error]', error);
              return {
                ...token,
                accessToken: user.accessToken,
                refreshToken: user.refreshToken,
                expiresAt: expiredTime,
                user: user.user,
              };
            }
          }
    
          if (Date.now() > (token.expiresAt as number)) {
            try {
              const response = await serverApi.post('users/auth/refresh', {
                headers: {
                  Authorization: `Bearer ${token.refreshToken}`,
                },
              });
    
              const data: LoginSuccessResponse = await response.json();
              return {
                ...token,
                accessToken: data.data.accessToken,
                expiresAt: getTokenExpiration(data.data.accessToken),
              };
            } catch (error) {
              console.error('[refresh error]', error);
              signOut();
              return null;
            }
          }
          return token;
        },
        // TODO: 추후 타입 수정
        session: async ({ session, token }) => {
          return {
            ...session,
            expiresAt: token.expiresAt,
            user: token.user,
            role: token.role,
          } as Session;
        },
        redirect: async ({ url, baseUrl }) => {
          if (url.startsWith('/')) return `${baseUrl}${url}`;
          if (url) {
            const { search, origin } = new URL(url);
            const callbackUrl = new URLSearchParams(search).get('callbackUrl');
            if (callbackUrl)
              return callbackUrl.startsWith('/')
                ? `${baseUrl}${callbackUrl}`
                : callbackUrl;
            if (origin === baseUrl) return url;
          }
          return baseUrl;
        },
      },
    });

     

     

    이 부분이 로그인 시 사용되게 된다.

    아래에 자신의 로그인 api 엔드포인트와 함께 응답값을 작성해 주면 된다.

      async authorize(credentials): Promise<any> {
            try {
              const response = await serverApi.post('users/auth/login', {
                json: credentials,
              });
              const data: LoginSuccessResponse = await response.json();
              const userResponse = await serverApi.get('users', {
                headers: {
                  Authorization: `Bearer ${data.data.accessToken}`,
                },
              });
              const userData: { data: { user: UserInfoType } } =
                await userResponse.json();
              return {
                accessToken: data.data.accessToken,
                refreshToken: data.data.refreshToken,
                user: userData.data.user,
              };
            } catch (error) {
              console.error('[authorize error]', error);
              return null;
            }
          },

     

     

    아래 부분은 매 요청 시 마다 실행되게 된다.

    크게 두가지 부분으로 나뉜다. 

     

    1. jwt 토큰을 저장하는 부분

    2. 토큰 만료 시 재발급하는 부분

     

     callbacks: {
        jwt: async ({ token, account, user }) => {
          // 소셜 로그인은 하지 않으므로 넘어간다.
          if (account?.provider === 'kakao') {
            return {
              ...token,
              accessToken: account.access_token,
              refreshToken: account.refresh_token,
            };
          }
    		// 커스텀 로그인을 여기서 분기한다.
          if (user && account?.provider === 'credentials') {
            const expiredTime = getTokenExpiration(user.accessToken as string);
    
            try {
              const headers = {
                Authorization: `Bearer ${user.accessToken}`,
              };
              const rolesRes = await serverApi.get('users/roles', { headers });
              const rolesData: RoleResponse = await rolesRes.json();
              console.log('token', token);
    
              return {
                ...token,
                accessToken: user.accessToken,
                refreshToken: user.refreshToken,
                expiresAt: expiredTime,
                user: user.user,
                role: rolesData.data.role,
              };
            } catch (error) {
              console.error('[role fetch error]', error);
              return {
                ...token,
                accessToken: user.accessToken,
                refreshToken: user.refreshToken,
                expiresAt: expiredTime,
                user: user.user,
              };
            }
          }
    	// 여기서 만료 여부를 확인하고 만료가 되었다면 다시 엑세스 토큰을 재발급한다.
          if (Date.now() > (token.expiresAt as number)) {
            try {
              const response = await serverApi.post('users/auth/refresh', {
                headers: {
                  Authorization: `Bearer ${token.refreshToken}`,
                },
              });
    
              const data: LoginSuccessResponse = await response.json();
              return {
                ...token,
                accessToken: data.data.accessToken,
                expiresAt: getTokenExpiration(data.data.accessToken),
              };
            } catch (error) {
              console.error('[refresh error]', error);
              signOut();
              return null;
            }
          }
          return token;
        },

     

     

    아래는 세션에 들어갈 정보이다.

    이때 주의할 점은 세션은 네트워크 탭에 보이기 때문에 토큰 같은 정보를 저장해서는 안된다.

     session: async ({ session, token }) => {
          return {
            ...session,
            expiresAt: token.expiresAt,
            user: token.user,
            role: token.role,
          } as Session;
        },

     

     

    이는 따로 쿠키에 정보를 저장하고 있어 js로 접근할 수 없기 때문에 요청을 보내 세션 미들웨어에서 가공 처리를 하여 세션 정보를 가져오는 것이다.

     

     

    설정을 했으니 사용 방법에 대해 알아보자.

     

     

    클라이언트 컴포넌트에서 세션 정보 불러오기

    아래와 같이 간단하게 사용할 수 있다.

    'use client';
    
    import { useSession } from 'next-auth/react';
    
    
    const { data: session, status } = useSession();

     

     

    서버 컴포넌트에서 토큰 넣어주기

    아래와 같이 공통 api 로직을 통해 작성할 수 있다.

    나는 ky를 써서 다음과 같이 작성했다.

    'use server';
    
    import 'server-only';
    import ky from 'ky';
    import { headers } from 'next/headers';
    import { getToken } from 'next-auth/jwt';
    
    async function authAPi() {
      const reqLike = {
        headers: { cookie: (await headers()).get('cookie') ?? '' },
      };
    
      const jwt = await getToken({
        req: reqLike,
        secret: process.env.NEXT_AUTH_SECRET,
      });
    
      if (!jwt) {
        return ky.create({
          prefixUrl: process.env.NEXT_PUBLIC_API_URL,
        });
      }
    
      const access = jwt.accessToken;
    
      console.log('access', access);
    
      return ky.create({
        prefixUrl: process.env.NEXT_PUBLIC_API_URL,
        hooks: {
          beforeRequest: [
            async (req) => {
              if (access && !req.headers.get('Authorization')) {
                req.headers.set('Authorization', `Bearer ${access}`);
              }
            },
          ],
        },
      });
    }
    export default authAPi;

     

     

    더 자세한 사용법 알아보기

    https://www.heropy.dev/p/MI1Khc

     

    Auth.js(NextAuth.js) 핵심 정리

    Auth.js(NextAuth.js)는 Next.js 프로젝트의 사용자 인증 및 세션 관리를 위한 라이브러리로 Google, GitHub 등의 다양한 인증 공급자를 지원하며, Next.js의 서버와 클라이언트 측 모두에서 인증 및 세션 관리

    www.heropy.dev

     

    반응형