- Published on
NextAuth OAuth 콜백 403·state mismatch 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 OAuth 로그인은 개발 환경에서는 잘 되다가도, 배포(특히 Vercel, Cloudflare, Nginx, ALB 같은 프록시/엣지)로 가면 갑자기 콜백이 403으로 떨어지거나 state mismatch가 터지는 경우가 많습니다. 원인은 대체로 단순합니다. NextAuth가 state를 쿠키에 저장해두고, 콜백 요청에서 그 쿠키를 다시 읽어 검증하는데, 쿠키가 누락되거나 다른 값으로 바뀌면 검증이 실패합니다.
이 글에서는 403과 state mismatch를 같은 문제군으로 묶어, 실제 운영에서 가장 흔한 원인과 해결책을 단계별로 정리합니다. (NextAuth v4 기준, App Router/Pages Router 모두 적용 가능한 형태로 작성)
증상 패턴: 403 vs state mismatch는 같은 계열
다음 중 하나라도 보이면 거의 동일한 진단 루틴으로 접근하면 됩니다.
- OAuth 제공자(구글, 깃허브 등) 인증 후 돌아오자마자
403응답 - 서버 로그에
OAuthCallbackError또는state mismatch문구 - 콜백 URL은 정상인데, 세션 쿠키/CSRF/PKCE 관련 쿠키가 콜백 요청에 실리지 않음
- 특정 브라우저(사파리, 모바일)에서만 재현
핵심은 이겁니다.
- 로그인 시작 시점: NextAuth가
state(및 PKCE code verifier 등)를 쿠키로 저장 - OAuth 제공자 인증 후 콜백 시점: NextAuth가 그 쿠키를 다시 읽어 state를 비교
- 쿠키가 없거나 도메인/경로/보안 속성 때문에 안 오면
state mismatch또는403
원인 1) NEXTAUTH_URL 불일치(가장 흔함)
NEXTAUTH_URL은 NextAuth가 콜백 URL, 쿠키 도메인/보안 정책을 결정하는 중요한 기준입니다. 다음 같은 불일치가 있으면 쿠키가 예상과 다르게 설정되거나, 리다이렉트 URL이 꼬입니다.
- 실제 접속은
https://app.example.com인데NEXTAUTH_URL은http://localhost:3000 www유무가 다름 (https://example.comvshttps://www.example.com)- 프록시 뒤에서 실제는
https인데 앱은http로 인식
해결
배포 환경에서 반드시 실제 외부 URL로 맞춥니다.
# .env.production
NEXTAUTH_URL="https://app.example.com"
NEXTAUTH_SECRET="...긴 랜덤 문자열..."
또한 OAuth 제공자 콘솔(예: Google Cloud Console)의 Authorized redirect URI도 정확히 일치해야 합니다.
https://app.example.com/api/auth/callback/google
http와 https, www 유무, 경로까지 1글자라도 다르면 문제를 유발합니다.
원인 2) 프록시/로드밸런서에서 X-Forwarded-Proto 누락
Nginx, ALB, Cloudflare 같은 프록시 뒤에서는 앱이 원래 요청이 https였는지 http였는지 헤더로 판단합니다. 이 정보가 없으면 NextAuth가 쿠키를 secure로 설정하지 않거나, 반대로 secure 쿠키가 필요한데 잘못 판단해 쿠키가 안 붙는 상황이 생깁니다.
Nginx 예시 설정
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:3000;
}
Cloudflare나 다른 CDN을 쓴다면, 원본 서버까지 X-Forwarded-Proto가 전달되는지 확인하세요.
원인 3) 쿠키 SameSite/도메인 문제로 콜백에 쿠키가 안 실림
state mismatch의 실질 원인은 “콜백 요청에 state 쿠키가 안 왔다”인 경우가 대부분입니다. 특히 아래 조건에서 자주 터집니다.
- 커스텀 도메인 전환 중 (preview 도메인과 production 도메인 혼용)
- 서로 다른 서브도메인 간 이동 (
auth.example.com에서 시작해app.example.com으로 콜백) - 사파리/모바일에서 ITP 영향으로 쿠키 정책이 더 엄격
진단 방법
Chrome DevTools에서 다음을 확인합니다.
- 로그인 시작 요청(예:
/api/auth/signin/google) 응답에Set-Cookie가 내려오는지 - 콜백 요청(예:
/api/auth/callback/google)에 해당 쿠키가 실려 가는지
쿠키 이름은 환경/설정에 따라 다르지만, NextAuth 관련 쿠키(예: next-auth.state, next-auth.csrf-token, PKCE 관련 쿠키 등)가 로그인 시작 시점에 생성됩니다.
해결 1: 동일한 호스트에서 시작하고 동일한 호스트로 콜백 받기
가장 안정적인 해결책은 OAuth 플로우 전체를 한 호스트에서 끝내는 것입니다.
- 시작:
https://app.example.com/api/auth/signin/... - 콜백:
https://app.example.com/api/auth/callback/...
서브도메인을 나누고 싶다면, 쿠키 도메인 정책을 명확히 해야 합니다.
해결 2: NextAuth 쿠키 설정 커스터마이즈
아래는 “운영에서 HTTPS, 프록시 뒤, 서브도메인 공유” 같은 조건에서 쿠키를 명시적으로 조정하는 예시입니다.
// pages/api/auth/[...nextauth].ts 또는 app 라우팅용 nextauth 설정 파일
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
const isProd = process.env.NODE_ENV === "production";
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
secret: process.env.NEXTAUTH_SECRET,
// 필요 시 쿠키 정책을 명시
cookies: {
sessionToken: {
name: isProd ? "__Secure-next-auth.session-token" : "next-auth.session-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: isProd,
// 서브도메인 공유가 필요하면 domain을 명시
// domain: ".example.com",
},
},
},
});
주의할 점
domain: ".example.com"을 넣으면app.example.com과auth.example.com이 쿠키를 공유할 수 있지만, 잘못 설정하면 오히려 쿠키 충돌/누락이 생깁니다.sameSite를 무작정none으로 바꾸면, 반드시secure: true가 필요합니다.- OAuth 콜백은 “서드파티에서 돌아오는 top-level navigation”이라 보통
sameSite: "lax"로도 충분한 경우가 많습니다.
원인 4) 프리뷰 도메인과 프로덕션 도메인 혼용
Vercel preview, Cloudflare preview, 사내 스테이징 도메인을 함께 쓰면 다음 문제가 생깁니다.
- 로그인 시작은 preview 도메인에서 했는데
- OAuth 제공자 redirect URI는 production 도메인으로 설정되어 있고
- 콜백은 production으로 들어오면서 preview에서 만든 쿠키가 당연히 없음
결과는 state mismatch 또는 403입니다.
해결
- 환경별로 OAuth 앱을 분리(스테이징용 OAuth 클라이언트, 운영용 OAuth 클라이언트)
- 환경별
NEXTAUTH_URL을 정확히 분리 - preview 환경에서는 OAuth 기능을 제한하거나, preview 도메인도 redirect URI로 등록
원인 5) NEXTAUTH_SECRET 변경/누락으로 토큰 검증 실패
NEXTAUTH_SECRET이 배포마다 바뀌거나 누락되면, 쿠키/토큰 암복호화 검증이 실패해 403 계열로 떨어질 수 있습니다.
해결
- 운영 환경에 고정된
NEXTAUTH_SECRET을 설정 - 여러 인스턴스(스케일 아웃)에서 동일한 시크릿을 공유
# 한 번 생성한 값을 안전하게 보관하고 모든 인스턴스에 동일하게 주입
NEXTAUTH_SECRET="openssl rand -base64 32" # 예시 명령
위 명령 자체를 그대로 쓰기보다, 실제로는 생성된 결과 문자열을 환경변수로 저장해 고정하세요.
원인 6) 시간 불일치(드물지만 치명적)
서버 시간이 크게 틀어지면 토큰 만료/검증 로직에서 문제가 날 수 있습니다. 특히 컨테이너/VM에서 NTP가 비활성화된 경우가 있습니다.
해결
- 노드가 동작하는 OS/컨테이너의 시간 동기화(NTP) 확인
- 쿠버네티스라면 노드 레벨 시간 동기화 확인
이 유형은 OAuth 자체의 state mismatch보다는 세션/토큰 403으로 나타나는 경우가 많습니다.
실전 디버깅: 로그와 네트워크에서 무엇을 봐야 하나
1) NextAuth 디버그 활성화
export default NextAuth({
debug: process.env.NODE_ENV !== "production",
// ...
});
개발/스테이징에서만 켜고, 운영에서는 민감 정보가 로그에 남지 않도록 주의합니다.
2) 브라우저에서 쿠키 흐름을 확인
/api/auth/signin/...응답 헤더에Set-Cookie가 내려오는지- 콜백
/api/auth/callback/...요청 헤더에Cookie가 실리는지 secure,sameSite,domain,path가 의도대로인지
이 과정에서 성능 이슈로 디버깅이 방해될 정도로 브라우저가 버벅인다면, 긴 태스크를 먼저 정리하는 것도 도움이 됩니다. 프론트가 멈추면 네트워크/스토리지 패널 확인 자체가 어려워지기 때문입니다. 필요하면 Chrome INP 급등? Long Task 추적·해결 가이드도 함께 참고하세요.
체크리스트: 콜백 403·state mismatch 10분 컷
NEXTAUTH_URL이 실제 외부 URL과 완전히 동일한가 (https,www, 경로)- OAuth 제공자 콘솔의 redirect URI가 정확한가
- 프록시가
X-Forwarded-Proto를 전달하는가 - 로그인 시작 도메인과 콜백 도메인이 동일한가
- preview/production 도메인이 섞이지 않았는가
NEXTAUTH_SECRET이 고정되어 있고 모든 인스턴스에서 동일한가- 쿠키
secure/sameSite/domain설정이 환경과 일치하는가 - 특정 브라우저(사파리)에서만 문제면 쿠키 정책/ITP 영향을 의심했는가
결론: 쿠키가 끊기면 state가 끊긴다
NextAuth의 OAuth 콜백 403과 state mismatch는 대부분 “콜백에서 검증에 필요한 쿠키를 못 읽는다”로 귀결됩니다. 그래서 해결도 쿠키가 끊기는 지점을 찾는 방식이 가장 빠릅니다.
- URL과 도메인을 정합하게 맞추고
- 프록시 헤더를 올바르게 전달하고
- 환경별 OAuth 설정을 분리하고
- 필요할 때만 쿠키 옵션을 명시적으로 조정
이 4가지만 지켜도 운영에서의 OAuth 콜백 장애를 크게 줄일 수 있습니다.