Deallo

인증 · 미들웨어

HTTPS 세션 만료 시 무한 리다이렉트 — __Secure- 쿠키

세션이 무효해진 사용자가 프로덕션(HTTPS)에 접속하면 ERR_TOO_MANY_REDIRECTS로 사이트 자체가 안 떴다. 쿠키를 지우라고 했는데 브라우저가 그 명령을 통째로 무시하고 있었다. 코드가 아니라 RFC 쿠키 규칙(__Secure- prefix)을 알아야 풀리는 P0였다.

증상

  • 프로덕션(HTTPS) 진입 시 ERR_TOO_MANY_REDIRECTS로 페이지가 아예 안 뜸
  • 시크릿 창·로컬 dev(http)에선 정상 — 특정 사용자만 막힘
  • 토큰 만료·권한 박탈·강제 로그아웃 등 BE가 401을 주는 모든 경우에 발생할 수 있는 시한폭탄 → P0

의도된 흐름

미들웨어가 모든 페이지 진입 전 토큰 유효성을 BE로 확인하고, 무효(401)면 세션 쿠키를 지우고 /signin으로 리다이렉트하도록 의도돼 있었다.

function buildSignOutRedirect(origin: string): NextResponse {
  const res = NextResponse.redirect(`${origin}/signin`);
  for (const name of SESSION_COOKIES) {
    res.cookies.delete(name); // ← 이게 안 먹었다
  }
  return res;
}

근본 원인 — 브라우저가 Set-Cookie를 무시

세션 쿠키 이름은 환경에 따라 둘이었다.

  • next-auth.session-token (http / dev)
  • __Secure-next-auth.session-token (https / prod)

cookies.delete(name)이 만드는 Set-Cookie 헤더엔 Secure 플래그가 없다.

Set-Cookie: __Secure-next-auth.session-token=; Path=/; Max-Age=0
                                                ↑ Secure 없음

RFC 6265bis엔 이런 규칙이 있다.

쿠키 이름이 __Secure-로 시작하면 Set-Cookie에 반드시 Secure 플래그가 있어야 한다. 없으면 그 헤더를 통째로 무시한다.

cookies.delete()의 헤더는 스펙 위반이라 브라우저가 무시 → 쿠키가 안 지워짐 → 다음 요청도 같은 토큰 → BE 또 401 → 또 삭제 시도(또 실패) → 무한 리다이렉트 루프. http(dev)는 __Secure-가 아니라 정상 동작해서 로컬에선 재현이 안 됐다.

해결

delete 대신 Secure 옵션을 명시한 set으로 빈 값을 덮어썼다.

res.cookies.set(name, "", {
  path: "/",
  maxAge: 0,
  secure: true, // __Secure- 접두사 규칙 충족
  httpOnly: true,
  sameSite: "lax",
});

배운 점

cookies.delete()는 만능이 아니다. 쿠키를 지우는 Set-Cookie도 원래 설정과 같은 속성(Secure · Path · SameSite)을 맞춰야 브라우저가 받아준다. "로컬은 되는데 프로덕션만 안 되는" 버그는 http/https 차이(Secure · SameSite)를 먼저 의심한다.