[Next.js] NextAuth v5, middleware
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에서 모두 쿠키 값을 가져올 수 있게 했다.
이를 NextAuth.js
는 HttpOnly
쿠키에 저장된 세션/토큰에 클라이언트에서도 “간접적으로” 접근할 수 있도록 프론트 서버(Next.js API)를 중계 지점으로 활용해 해결했다.
클라이언트에서 /api/auth/session
에 fetch 요청을 하면 프론트 서버는 요청에 포함된 HttpOnly
쿠키를 보고 세션 유효성 검사를 한다. 유효하면 session을 반환하여 client에서도 읽을 수 있다.
🔥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.ts
에 basePath: '/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와 하이드레이션까지 연동하는 실전 예제를 다룰 예정이다.
댓글남기기