7 분 소요

NextAuth로 Access Token 관리하기


관련 PR
middleware, logout 기능 추가
/api 경로 못 찾는 오류 해결


🔥1. NextAuth 도입 이유

1. SSR에서는 LocalStorage에 접근할 수 없다

Next에서는 SSR와 CSR 환경 모두에서 토큰을 가져와 api를 요청할 수 있어야 한다. 이전 프로젝트처럼 Access Token을 localStorage에 저장하고 보니 아래와 같은 오류가 떴다.

ReferenceError: localStorage is not defined

SSR에서는 LocalStorage에 접근할 수 없고 window, document 같은 BOM 또한 사용할 수 없다. 서버에서 실행되기 때문이다.


2. 토큰을 쿠키에 저장하자

그래서 나온 해결책이 토큰을 쿠키에 저장하는 방법이다.

페이지를 SSR로 생성할 때 브라우저는 서버에 HTTP 요청을 보내는데 이때 쿠키 값이 자동으로 포함된다. 이때 SSR에서 쿠키에 있는 값을 읽을 수 있다.


3. 토큰을 쿠키에 안전하게 저장하자

HttpOnly로 토큰을 쿠키에 저장하면 client에서는 쿠키에 있는 값을 가져올 수 없다. 하지만 HttpOnly 옵션을 제거하면 보안이 약해진다. 따라서 NextAuth.js를 사용해서 보안 옵션은 다 적용하고 client와 server에서 모두 쿠키 값을 가져올 수 있게 했다.

Image

이를 NextAuth.jsHttpOnly 쿠키에 저장된 세션/토큰에 클라이언트에서도 “간접적으로” 접근할 수 있도록 프론트 서버(Next.js API)를 중계 지점으로 활용해 해결했다.

클라이언트에서 /api/auth/session에 fetch 요청을 하면 프론트 서버는 요청에 포함된 HttpOnly 쿠키를 보고 세션 유효성 검사를 한다. 유효하면 session을 반환하여 client에서도 읽을 수 있다.

Image



🔥2. NextAuth v5 설치

참고: Upgrade Guide (NextAuth.js v5)

NextAuth beta 버전이 5이다. 버전 5는 앱라우터에 최적이기 때문에 Next.js 14버전 이상부터 사용할 수 있다.

npm install next-auth@beta



🔥3. AUTH_SECRET 생성

AUTH_SECRET는 JWT 서명 및 검증, 암호화된 세션 쿠키 생성, 이메일 인증 토큰 해시 등을 위한 가장 중요한 환경 변수이다.

bash에 아래와 같이 입력하면 32바이트 시크릿 키가 나온다. 그걸 env에 넣어주면 된다.

openssl rand -base64 32

.env

AUTH_SECRET=시크릿키



🔥4. auth.ts 파일 생성

auth.ts 파일

/src/auth.ts 파일을 생성하고 아래와 같이 코드를 작성한다.

import NextAuth from "next-auth";

export const { auth, handlers, signIn, signOut } = NextAuth({
  session: {},
  providers: [],
  callbacks: {},
  session: {},

  secret: process.env.AUTH_SECRET, // 비밀키 작성
});


1. 인증 함수 export

첫 줄 부터 찬찬히 알아보자.

NextAuth v5(App Router 전용)의 설정 방식으로, 핵심 인증 함수들을 NextAuth()로 초기화한 후, auth, handlers, signIn, signOut 등을 서버/클라이언트에서 각각 적절히 사용할 수 있도록 export하는 구조이다.

export const { auth, handlers, signIn, signOut } = NextAuth({});


1. auth

HttpOnly 쿠키를 파싱해서 유저 정보를 반환

  • app/api/* API 핸들러, middleware.ts, 서버 컴포넌트에서 사용
  • 서버 컴포넌트에서 사용 예시

    import { auth } from "@/auth";
    
    const session = await auth();
    


2. handlers

인증 관련 요청을 처리하는 엔드포인트 핸들러

  • app/api/auth/[...nextauth]/route.ts에서 사용
  • /api/auth/session 요청 응답
  • /api/auth/signin, /signout, /callback 등 모든 인증 요청 처리

NextAuth v5에서는 인증 관련 요청(/api/auth/session, /api/auth/signin, /api/auth/callback 등)을 처리하기 위해 핸들러(route 파일) 를 직접 등록해야 한다.

app/api/auth/[…nextauth]/route.ts 등록

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

내부적으로 NextAuth는 이 route를 기반으로 다음 요청을 처리한다.

GET /api/auth/session
POST /api/auth/signin
GET /api/auth/callback/github 등


3. signIn

클라이언트에서 로그인 시도

  • 로그인 성공 -> 쿠키에 HttpOnly 세션 저장
import { signIn } from "next-auth/react";

await signIn("credentials", {
  accessToken: data.accessToken,
  callbackUrl: "/",
});


4. signOut

클라이언트에서 로그아웃 수행

  • 쿠키 삭제
import { signOut } from "next-auth/react";

await signOut({ callbackUrl: "/login" });


2. session

NextAuth의 세션 관리 방식을 설정하는 코드

  • strategy : 'jwt' | 'database'
    jwt: 세션 데이터를 JWT 토큰에 인코딩해서 쿠키로 저장 (stateless)
    database: 세션 데이터를 데이터베이스에 저장, 쿠키엔 세션 ID만 저장 (stateful)
  • maxAge: 세션 유효 시간 (초 단위)
session: {
  strategy: 'jwt',
  maxAge: 60 * 60 * 24, // 1일
}


3. providers

어떤 방식으로 로그인할 것인지 정의하는 곳

providers: [
  Credentials({...}),
  GoogleProvider({...}),
  GitHubProvider({...}),
  ...
]


Credentials provider란?

  • name : 로그인 페이지에서 보여질 provider 이름
  • credentials : 로그인 폼 필드 정의
  • authorize() : 유저 인증 로직 수행, signIn()할 때 실행되는 함수, user 정보를 반환
providers: [
  Credentials({
    name: 'Credentials',
    credentials: {
      accessToken: { label: 'Access Token', type: 'text' },
    },
    authorize: async (credentials) => {
      if (!credentials.accessToken || typeof credentials.accessToken !== 'string') {
        throw new Error('Invalid access token');
      }

      // user 정보 반환
      return {
        id: credentials.accessToken, // 유저를 고유하게 식별하는 키(필수)
        accessToken: credentials.accessToken,
      } as User;
    },
  }),
],


4. callbacks

NextAuth의 callbacks는 인증 과정 중간에 개입할 수 있는 훅(Hook) 함수들의 집합이다.
이를 통해 JWT 토큰이나 세션 객체의 구조를 직접 가공할 수 있다.
accessToken을 JWT 내부에 저장하거나, 클라이언트 세션 응답에 특정 값을 추가할 수 있다.

  • 로그인 후 받은 유저 정보(user)를 JWT payload(token)에 추가
  • JWT에서 꺼낸 정보를 세션 응답(session)으로 구성하는 등의 작업을 수행


1. jwt 콜백

✅ 이 콜백은 JWT를 어떻게 구성할지 정의하는 단계

  • JWT 세션 전략(session.strategy: 'jwt')을 사용하는 경우, 로그인 성공 시 authorize()에서 반환한 user 객체가 jwt()로 전달됩니다.
  • 이 콜백에서 반환한 token 객체는 JWT의 payload로 사용되며, NextAuth가 이를 서명(sign)하여 쿠키에 저장
  • 이후 클라이언트 요청이 들어오면, NextAuth는 쿠키에서 JWT를 읽어 복호화하고, 그 결과를 다시 token으로 제공하여 jwt()를 요청마다 호출
  • JWT의 exp를 이용해 accessToken 만료 여부를 검사하거나, 필요한 경우 토큰 재발급 로직을 넣을 수 있음

  • user : 로그인 직후 한 번만 존재(authorize()에서 리턴한 user 객체)
  • token : NextAuth가 내부적으로 생성·유지하는 JWT의 payload 객체


2. session 콜백

✅ 이 콜백은 클라이언트에게 어떤 세션 데이터를 보여줄지 정의하는 단계

  • 클라이언트가 useSession() 훅을 사용하거나 /api/auth/session 요청을 보낼 때 실행
  • 이때 NextAuth는 쿠키에서 JWT를 읽고 복호화하여 token으로 전달
  • 우리는 여기서 JWT 내부 정보(token.accessToken 등)를 클라이언트 세션 응답(session)에 추가 가능
  • 최종적으로 클라이언트는 useSession()을 통해 가공된 세션 데이터를 받아 사용

  • session : 클라이언트에 반환될 세션 객체
  • token : 복호화된 JWT의 payload(callbacks.jwt()에서 리턴한 내용)
callbacks: {
  jwt: ({ token, user }) => {
    if (user) { // 처음 로그인 할 때 token에 authorize에서 반환한 user 정보 저장
      token.accessToken = user.accessToken;
      const { exp } = jwtDecode<{ exp: number }>(user.accessToken);
      token.accessTokenExpires = exp * 1000;
    }

    if (Date.now() < (token.accessTokenExpires ?? 0)) {
      return token;
    }

    return {};
  },

  session: ({ session, token }) => {
    session.accessToken = token.accessToken as string; // 커스텀 필드 추가
    return session;
  },
},


nextAuth.d.ts - 커스텀 필드 추가

import "next-auth";
import "next-auth/jwt";

declare module "next-auth" {
  interface Session {
    accessToken?: string;
  }
  interface User {
    accessToken: string;
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    accessToken?: string;
    accessTokenExpires?: number;
  }
}


전체 흐름

1. 로그인 성공
  ↳ authorize() → user 리턴
  ↳ jwt({ token, user }) → JWT payload 구성
  ↳ JWT 서명 후 쿠키에 저장

2. 이후 요청 보내면
  ↳ JWT 쿠키 자동 포함 → 서버에서 복호화
  ↳ jwt({ token }) → 만료 여부 등 검증
  ↳ session({ session, token }) → 세션 응답 구성
  ↳ 클라이언트는 useSession()으로 세션 데이터 확인



🔥5. Universal auth()

NextAuth v5에서는 auth()라는 단일 API를 통해 서버 측에서도 인증 상태를 쉽게 확인할 수 있다. 이 함수는 JWT 쿠키를 자동으로 읽고 복호화한 뒤, 사용자 정보를 반환한다.

1. sever

v5에서는 @/auth에서 import 한다.

import { auth } from "@/auth";

const session = await auth();
console.log(session?.accessToken);


2. client

getSession 함수가 사라지고 v5에서는 useSession만 권장된다.
하지만 util함수에서는 useSession을 못 쓰므로 직접 api를 요청하여 session을 받아온다.

const res = await fetch("/api/auth/session");
const session = await res.json();
console.log(session?.accessToken);


3. client component

[status] 문자열 리터럴

  • loading: 세션 정보를 가져오는 중일 때
  • unauthenticated: 세션이 없어서 로그인 상태가 아닐 때
  • authenticated: 세션이 존재해서 로그인된 상태일 때
"use client";

import { useSession } from "next-auth/react";

const { data: session, status } = useSession();
const isLogin = status === "authenticated" && !!session?.user;


4. middleware

import { auth } from "@/auth";

export default auth((req) => {
  // 미들웨어 코드
});



🔥6. SessionStorage 추가

NextAuth v5에서 클라이언트 컴포넌트에서 세션 정보를 사용하려면, useSession() 훅을 사용해야 한다.
하지만 이 훅이 동작하려면 반드시 상위에 SessionProvider로 context가 감싸져 있어야 한다.


공식 문서: SessionProvider에 session prop 주입

참고: Upgrade Guide (v4) | NextAuth.js

공식 문서에서 “초기 세션을 주입하면 첫 화면에서 깜빡임(로딩) 없이 UI를 그릴 수 있다 라고 되어 있어서 session을 상위에서 호출해 provider 인자에 넣어주었다.

// `session` comes from `getServerSideProps` or `getInitialProps`.
// Avoids flickering/session loading on first load.
<SessionProvider session={session} refetchInterval={5 * 60}>
  <Component {...pageProps} />
</SessionProvider>


실제 코드

providers/AuthProvider.tsx

export const AuthProvider = ({ children, session }: AuthProviderProps) => {
  return (
    <SessionProvider
      session={session} // session 주입
      refetchOnWindowFocus={false}
    >
      {children}
    </SessionProvider>
  );
};


app/layout.tsx

const RootLayout = async ({ children }) => {
  const session = await auth(); // session을 AuthProvider에 전달

  return (
    <html lang="ko" className={`${pretendard.variable}`}>
      <body className={pretendard.className}>
        <AuthProvider session={session}>{children}</AuthProvider>
      </body>
    </html>
  );
};



🔥6. middleware 생성

인증이 필요한 페이지/경로에 접근 제한하고 싶을 때 middleware에서 인증을 체크하고 라우팅할 수 있다.

  • auth() 내부에서 JWT 쿠키를 자동 복호화하여 인증 정보 확인
  • req.auth가 없으면 로그인되지 않은 상태 -> “/signin” 페이지로 리디렉션
  • matcher : middleware 실행 대상 경로, 보호 대상 페이지
import { NextResponse } from "next/server";
import { auth } from "@/auth";

const SIGNIN_PATH = "/signin";

export default auth((req) => {
  if (!req.auth) {
    return NextResponse.redirect(new URL(SIGNIN_PATH, req.url));
  }
  return NextResponse.next();
});

export const config = {
  matcher: [
    "/mypage/:path*",
    "/home/:stadiumId/review/:path*",
    "/settings/:path*",
  ],
};



🤔basePath 변경하기

[문제점]

아래와 같은 오류가 떴다.

GET https://concertseat.site/api/auth/session 400 (Bad Request)
body: null
header: {message: "No static resource api/auth/session."}


[원인]

로컬에서 잘 되고, 예전에 나의 다른 버셀 도메인으로 배포했을 때도 잘 됐는데 https://concertseat.site/ 여기서만 안 됐다.

이것으로 짐작했을 때 백엔드 api 요청도 https://concertseat.site/api로 보내고 있는데 이걸 Next가 프론트 서버와 구분을 못 하고 있는 것 같았다.


[해결 방법]

백엔드 서버 url을 바꾸는 건 비용이 매우 크니 프론트 서버 url을 /api가 아닌 다른 것으로 변경했다.

참고: https://authjs.dev/reference/nextjs#basepath

원래는 basePath가 app/api/auth/[...nextauth]/route.ts라서 설정을 생략하지만 app/auth/[...nextauth]/route.ts로 변경할 예정이기 때문에 3개 설정을 추가해준다.

1. src/auth.tsbasePath: '/auth' 추가

export const { auth, handlers, signIn, signOut } = NextAuth({
  session: {},
  providers: [],
  callbacks: {},

  secret: process.env.AUTH_SECRET,
  basePath: "/auth", // 추가
} as NextAuthConfig);


2. SessionProvider 속성에 basePath="/auth" 추가

<SessionProvider
  session={session}
  basePath="/auth" // 추가
  refetchOnWindowFocus={false}
>
  {children}
</SessionProvider>


3. AUTH_URL 뒤에 /auth 추가

위에서 AUTH_SECRET를 생성할 때 AUTH_URL은 env에 추가하지 않았다. 없으면 기본적으로 https://your-domain.com/api/auth에 api 요청을 하기 때문이다. 하지만 지금은 basePath가 바뀌었으므로 변경한 후에 env에 추가해준다.

AUTH_URL=https://concertseat.site/auth



🔥마무리

이번 글에서는 NextAuth의 초기 설정 과정과, 서버·클라이언트 전반에서 세션을 안전하게 관리하는 방법까지 정리해보았다.
auth(), callbacks, SessionProvider, middleware 등 NextAuth v5에서의 인증 흐름에 대해 공부한 후 프로젝트에 적용하였다.

다음 글에서는 이 인증 구조를 바탕으로, 서버와 클라이언트 양쪽에서 인증된 API 요청을 처리하는 universal API 구성, 그리고 Tanstack Query와 하이드레이션까지 연동하는 실전 예제를 다룰 예정이다.

카테고리:

업데이트:

댓글남기기