- Published on
NextAuth.js JWT 세션 401 반복 시 점검 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 로그인에 성공했다고 말하는데, 다음 요청부터 API가 401을 반복하는 케이스는 NextAuth.js에서 꽤 흔합니다. 특히 JWT 세션 전략을 사용할 때는 토큰이 발급은 되었으나 요청에 실려가지 않거나, 검증 단계에서 다른 키로 검증되거나, 미들웨어/프록시가 쿠키를 날려버리는 식으로 문제가 터집니다.
이 글은 “왜 401이 반복되는지”를 재현 지점별로 잘라서, 실제로 어디를 보면 되는지 7가지 체크리스트로 정리합니다. App Router 기준 예시를 포함합니다.
1) 세션 전략과 토큰 저장 위치부터 확정하기
NextAuth.js는 크게 두 가지 세션 전략을 씁니다.
strategy: "jwt": 세션 상태가 DB가 아니라 JWT + 쿠키로 유지strategy: "database": DB 세션을 조회
401 반복은 대개 jwt에서 발생합니다. 먼저 설정을 명시해서 혼선을 제거합니다.
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
export const authOptions = {
session: {
strategy: "jwt",
// maxAge: 30 * 24 * 60 * 60,
},
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
그리고 “어디서 401이 나오는지”를 구분하세요.
getServerSession이null인가auth()가null인가getToken()이null인가- 백엔드 API가
Authorization을 못 받는가
이 구분만 되어도 원인의 절반은 좁혀집니다.
2) NEXTAUTH_SECRET 불일치 또는 런타임별 시크릿 분리
JWT 검증은 NEXTAUTH_SECRET에 강하게 의존합니다. 다음 상황에서 401이 반복됩니다.
- 로컬에서는 되는데 배포에서만 401
- 서버리스 환경에서 인스턴스마다 시크릿이 다름
NEXTAUTH_SECRET이 비어 있어 매 배포 때 랜덤으로 바뀌는 것처럼 동작
점검 포인트:
- 모든 런타임(로컬, 프리뷰, 프로덕션)에서
NEXTAUTH_SECRET이 고정인지 - Edge 런타임과 Node 런타임이 섞여 있지 않은지
# 프로덕션에서 반드시 고정된 값
NEXTAUTH_SECRET="...긴 랜덤 문자열..."
NEXTAUTH_URL="https://your-domain.com"
추가로, Next.js App Router에서 미들웨어를 Edge로 돌리면서 토큰을 읽는다면, 같은 시크릿이 Edge에서도 접근 가능한지 확인해야 합니다.
3) 쿠키가 브라우저에 저장되지 않거나 요청에 포함되지 않는 문제
JWT 세션은 결국 쿠키가 실려야 합니다. 401 반복의 가장 흔한 원인은 “쿠키가 아예 안 실림”입니다.
대표 원인:
- 도메인/서브도메인 불일치
NEXTAUTH_URL이 실제 접속 URL과 다름secure및sameSite설정으로 인해 크로스 사이트 요청에서 쿠키가 차단- 프록시(Cloudflare, ALB, Nginx) 뒤에서
X-Forwarded-Proto가 잘못 전달되어 secure 쿠키가 무효화
브라우저 DevTools에서 다음을 확인하세요.
__Secure-next-auth.session-token또는next-auth.session-token쿠키 존재 여부- 쿠키의 Domain, Path, SameSite, Secure 속성
- API 요청에
Cookie헤더가 포함되는지
커스텀 쿠키 설정 예시:
export const authOptions = {
cookies: {
sessionToken: {
name:
process.env.NODE_ENV === "production"
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
},
},
},
}
만약 프론트와 백엔드가 다른 도메인이라면, 브라우저가 쿠키를 보내도록 fetch에 credentials: "include"가 필요합니다.
await fetch("https://api.your-domain.com/me", {
method: "GET",
credentials: "include",
})
CORS도 같이 맞춰야 합니다.
4) jwt/session 콜백에서 토큰이 누락되거나 덮어써지는 문제
NextAuth.js에서 JWT에 커스텀 값을 넣으려다, 실수로 기존 토큰을 덮어써서 인증정보가 사라지는 경우가 많습니다.
안티 패턴 예:
jwt콜백에서return { accessToken }처럼 최소 필드만 반환session콜백에서session.user = token.user처럼 구조가 안 맞는데 강제 대입
권장 패턴:
export const authOptions = {
callbacks: {
async jwt({ token, user, account }) {
// 최초 로그인 시
if (account && user) {
token.userId = user.id
token.provider = account.provider
token.accessToken = account.access_token
token.accessTokenExpires = account.expires_at
}
// 항상 기존 token을 기반으로 확장
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.userId as string
}
;(session as any).accessToken = token.accessToken
return session
},
},
}
401 반복이라면 다음을 로그로 확인하세요.
jwt콜백이 매 요청마다 호출되는지token이 어떤 시점에undefined필드로 바뀌는지
서버 로그를 찍을 때는 민감값은 마스킹하세요.
5) API 라우트/서버 액션에서 토큰 읽는 방식이 런타임과 불일치
App Router에서 흔한 실수는 다음입니다.
getServerSession을 써야 하는데 클라이언트에서 세션을 기대함- Edge 런타임에서 Node 전용 API를 사용
getToken()을 쓰는데req객체 타입이 맞지 않음
API Route에서 JWT를 확인하는 안전한 예시는 getToken()입니다.
// app/api/me/route.ts
import { NextResponse } from "next/server"
import { getToken } from "next-auth/jwt"
export async function GET(req: Request) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
if (!token) {
return NextResponse.json({ message: "unauthorized" }, { status: 401 })
}
return NextResponse.json({ userId: token.userId })
}
여기서 token이 null이면 쿠키 미포함, 시크릿 불일치, 혹은 미들웨어가 쿠키를 제거했을 가능성이 큽니다.
또한 미들웨어로 보호 중이라면, 미들웨어가 실제로 어떤 경로를 막고 있는지 점검하세요.
// middleware.ts
export { default } from "next-auth/middleware"
export const config = {
matcher: ["/dashboard/:path*", "/api/private/:path*"]
}
matcher가 과하게 넓으면 인증 라우트나 정적 리소스까지 막아서, 결과적으로 로그인 직후에도 401이 반복되는 것처럼 보일 수 있습니다.
6) 리프레시 토큰/액세스 토큰 만료 처리 누락
OAuth 공급자를 붙였을 때, NextAuth 세션은 살아있는데 실제 API 호출은 공급자 액세스 토큰 만료로 401이 발생할 수 있습니다. 이 경우 401의 주체는 NextAuth가 아니라 “외부 API”입니다.
증상:
getServerSession()은 정상- 하지만
session.accessToken으로 호출하는 외부 API가 401
해결은 jwt 콜백에서 만료를 감지해 refresh 로직을 넣는 것입니다.
async function refreshAccessToken(token: any) {
// provider별 refresh 구현 필요
// token.refreshToken을 사용해 새 access token을 받아온 뒤
// token.accessToken, token.accessTokenExpires 갱신
return {
...token,
accessToken: "new",
accessTokenExpires: Math.floor(Date.now() / 1000) + 3600,
}
}
export const authOptions = {
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
token.refreshToken = account.refresh_token
token.accessTokenExpires = account.expires_at
return token
}
const now = Math.floor(Date.now() / 1000)
if (token.accessTokenExpires && now < (token.accessTokenExpires as number)) {
return token
}
return refreshAccessToken(token)
},
},
}
이걸 빼먹으면 “처음엔 되다가 곧 401이 반복” 패턴이 나옵니다.
7) 시간 동기화, 프록시 헤더, 배포 환경 차이로 인한 간헐적 401
JWT는 시간에 민감합니다. 다음이 겹치면 간헐적으로 401이 반복됩니다.
- 컨테이너/노드의 시간이 몇 분씩 틀어짐
- 멀티 리전/멀티 노드에서 시간 편차
- 프록시가
X-Forwarded-Proto를 잘못 설정해 secure 쿠키 발급이 꼬임 - 서버리스 콜드 스타트 시 환경변수 로딩 문제
점검 방법:
- 서버에서
date확인, NTP 동기화 여부 확인 - 프록시(Nginx/ALB/Cloudflare)에서
X-Forwarded-Proto: https전달 확인 - 배포 환경에서
NEXTAUTH_URL이 실제 외부 URL과 일치하는지 재확인
이 범주의 문제는 “특정 네트워크/브라우저/리전에서만 재현”되는 경우가 많습니다. 인프라 레벨에서 에러가 간헐적으로 반복될 때의 접근법은 아래 글의 트러블슈팅 흐름도 참고할 만합니다.
재현을 빠르게 만드는 디버깅 체크리스트
401을 “감”으로 잡으면 오래 걸립니다. 아래 순서로 증거를 모으세요.
- 브라우저에서 로그인 직후 쿠키가 생겼는지 확인
- 401이 나는 요청에 쿠키가 포함되는지 확인
- 서버에서
getToken()이null인지 확인 NEXTAUTH_SECRET,NEXTAUTH_URL이 배포 환경에서 올바른지 확인jwt/session콜백에서 토큰을 덮어쓰지 않는지 확인- 외부 API 401인지, NextAuth 보호 라우트 401인지 구분
- 프록시 헤더 및 시간 동기화 확인
결론: 401 반복은 대부분 “쿠키” 또는 “시크릿”이다
NextAuth.js JWT 세션에서 401이 반복되면, 실제로는 다음 두 축으로 거의 수렴합니다.
- 쿠키가 클라이언트에 저장되지 않거나 서버로 전달되지 않는다
- 서버가 다른 시크릿/다른 조건으로 JWT를 검증하고 있다
그 다음으로는 콜백에서 토큰을 잘못 구성하거나, 액세스 토큰 만료를 처리하지 않아 외부 API가 401을 내는 케이스가 많습니다.
위 7가지를 순서대로 점검하면 “왜 로그인했는데도 계속 401인가”를 재현 가능한 문제로 바꿀 수 있고, 수정도 훨씬 빨라집니다.