Published on

Chrome 쿠키 차단으로 OAuth 로그인 루프 해결

Authors

서론

Chrome에서 갑자기 OAuth 로그인이 무한 리다이렉트(로그인 화면으로 되돌아감)를 일으키는 경우가 있습니다. 서버 로그에는 정상적으로 콜백이 들어오고 토큰 교환도 성공한 것처럼 보이는데, 브라우저에서는 세션이 유지되지 않아 다시 로그인으로 튕깁니다.

이 현상은 대개 쿠키가 저장되지 않거나, 저장되었더라도 다음 요청에 쿠키가 포함되지 않아 발생합니다. 특히 Chrome의 서드파티 쿠키 차단 강화, SameSite 기본값 변화, Secure 강제, 프록시/로드밸런서 뒤에서의 스킴 인식 문제 등이 조합되면 “OAuth 성공인데 세션 없음” 상태가 만들어집니다.

이 글에서는 Chrome에서 쿠키가 차단되는 대표 원인을 빠르게 판별하고, Next.js/Node/프록시 환경에서 재현·진단·해결하는 방법을 코드 중심으로 정리합니다. Next.js 환경의 무한 리다이렉트 자체에 대한 더 넓은 체크리스트는 Next.js OAuth 로그인 무한 리다이렉트 해결 가이드도 함께 참고하면 좋습니다.

증상 패턴: “콜백은 성공, 다음 요청은 비로그인”

다음 패턴이면 쿠키 문제일 확률이 높습니다.

  • OAuth 제공자에서 콜백 URL로 돌아온 뒤, 앱이 Set-Cookie로 세션을 심는다.
  • 직후 페이지 이동 또는 API 호출에서 세션 쿠키가 서버로 전달되지 않는다.
  • 서버는 비로그인으로 판단해 다시 OAuth 시작 엔드포인트로 리다이렉트한다.
  • 결과적으로 로그인 루프가 된다.

Chrome DevTools에서 빠르게 확인할 포인트는 아래 두 가지입니다.

  • Network 탭에서 콜백 응답에 Set-Cookie가 있는지
  • Application 탭의 Cookies에서 해당 쿠키가 실제로 저장되었는지(또는 “차단됨” 사유가 표시되는지)

Chrome이 쿠키를 막는 대표 원인 6가지

1) SameSite 기본값으로 인해 크로스 사이트 콜백에서 쿠키가 누락

OAuth는 본질적으로 “다른 사이트(제공자)에서 우리 사이트로 돌아오는” 흐름이 포함됩니다. 이때 쿠키가 크로스 사이트 문맥에서 전송되려면 보통 SameSite=None이 필요합니다.

  • SameSite=Lax: 일반적인 top-level navigation에서는 어느 정도 동작하지만, 특정 흐름(iframe, popup, XHR, POST 콜백 등)에서 문제가 생길 수 있습니다.
  • SameSite=Strict: 크로스 사이트에서는 거의 전송되지 않습니다.
  • SameSite=None: 크로스 사이트 전송 허용(단, Secure 필수)

특히 “OAuth 콜백에서 세션 쿠키를 심고, 즉시 API를 호출하는” 구조라면 Lax로는 부족한 경우가 많습니다.

2) SameSite=None인데 Secure가 없어서 Chrome이 거부

Chrome은 SameSite=None 쿠키에 대해 Secure 속성이 없으면 저장 자체를 거부합니다. 로컬 개발에서 http://localhost로 테스트할 때 자주 터집니다.

  • 개발: HTTPS가 아니라면 SameSite=None; Secure를 못 붙이거나, 붙여도 브라우저가 전송하지 않는 환경이 생길 수 있음
  • 운영: TLS 종단이 로드밸런서에서 일어나고 앱은 HTTP로 받는 경우, 앱이 “나는 HTTP야”라고 착각해 Secure를 누락하기도 함

3) 도메인/서브도메인 불일치로 쿠키 스코프가 맞지 않음

예를 들어 앱은 app.example.com인데 콜백은 auth.example.com이거나, API는 api.example.com인 경우가 있습니다.

  • 쿠키가 app.example.com에만 저장되면 api.example.com 요청에는 안 붙습니다.
  • 이때는 Domain=.example.com 같은 상위 도메인 스코프가 필요할 수 있습니다.

단, 불필요하게 상위 도메인으로 넓히면 보안 범위가 커지므로 최소 범위로 설정하세요.

4) Path가 좁아서 다음 요청에 쿠키가 안 붙음

세션 쿠키를 Path=/api/auth 같은 형태로 발급하면, 이후 / 또는 /dashboard로 이동할 때 쿠키가 전송되지 않습니다. 인증 쿠키는 보통 Path=/가 안전합니다.

5) 프록시/로드밸런서 뒤에서 X-Forwarded-Proto 미설정

TLS는 로드밸런서에서 끝나고 앱 서버는 HTTP로 받는 구조에서, 앱은 “내가 HTTP로 서비스 중”이라고 판단해 Secure 쿠키를 발급하지 않습니다. 결과적으로 Chrome이 쿠키를 거부하거나, HTTPS 페이지에서 HTTP 쿠키 정책이 꼬입니다.

해결은 프레임워크에 “프록시를 신뢰하고 forwarded 헤더를 읽어라”를 설정하고, 로드밸런서가 X-Forwarded-Proto: https를 전달하도록 하는 것입니다.

6) 서드파티 쿠키 차단 정책(특히 iframe 기반 로그인)

로그인이 iframe 안에서 진행되거나, 서로 다른 사이트 컨텍스트에서 세션을 심어야 하는 구조라면 Chrome의 서드파티 쿠키 차단에 직접 맞습니다.

  • “우리 도메인으로 돌아왔는데도” 브라우저가 여전히 third-party 컨텍스트로 판단하는 케이스가 있습니다(임베딩, 위젯, SSO 포털 등).
  • 이 경우는 단순 SameSite=None만으로 해결이 안 되고, 아키텍처를 top-level redirect 기반으로 바꾸거나, 가능한 경우 CHIPS나 Storage Access API 같은 대안을 검토해야 합니다.

빠른 진단 체크리스트(DevTools 기준)

  1. 콜백 응답의 Set-Cookie 확인
  • Network 탭에서 콜백 응답 선택
  • Response Headers에서 Set-Cookie가 있는지
  • SameSite, Secure, Domain, Path, HttpOnly 확인
  1. 쿠키 저장 여부와 차단 사유 확인
  • Application 탭 Cookies
  • 쿠키가 없거나, 경고 아이콘과 함께 “차단됨” 사유가 표시될 수 있음
  1. 다음 요청에 Cookie 헤더가 붙는지 확인
  • 콜백 이후 첫 API 요청 또는 페이지 요청
  • Request Headers에 Cookie가 포함되는지
  1. 서버 로그에서 “세션 생성”과 “세션 조회 실패”의 간격 확인
  • 콜백 처리 로그에서 세션 생성 ID를 찍고
  • 다음 요청에서 해당 세션 ID가 들어오는지 확인

해결 1: 세션 쿠키 속성 정석 세팅

다음은 Node/Express 스타일로 “크로스 사이트 OAuth를 고려한” 쿠키 예시입니다. 핵심은 SameSite=NoneSecure=true(운영), 그리고 Path=/입니다.

// Express 예시
app.get('/auth/callback', async (req, res) => {
  const sessionId = await createSessionForUser(/* ... */)

  const isProd = process.env.NODE_ENV === 'production'

  res.cookie('session', sessionId, {
    httpOnly: true,
    secure: isProd, // 운영에서는 반드시 true
    sameSite: isProd ? 'none' : 'lax',
    path: '/',
    // domain: '.example.com', // 필요할 때만
    maxAge: 7 * 24 * 60 * 60 * 1000,
  })

  res.redirect('/')
})

운영에서 SameSite=None을 쓴다면 secure: true가 사실상 필수입니다. 로컬 개발에서 재현이 어려우면 HTTPS 로컬 환경을 구성하거나(예: 로컬 리버스 프록시), 개발에서는 Lax로 타협하고 운영에서만 None을 쓰는 방식으로 분리하세요.

해결 2: Next.js에서 쿠키 설정 시 흔한 함정

Next.js App Router에서 cookies().set을 쓰는 경우에도 속성 원칙은 같습니다. 또한 “어디에서 쿠키를 세팅하느냐”가 중요합니다.

  • OAuth 콜백을 처리하는 Route Handler에서 쿠키를 세팅
  • 그 응답이 실제로 브라우저에 도달해야 함
  • 중간에 다른 리다이렉트/프록시가 Set-Cookie를 제거하지 않아야 함
// Next.js Route Handler 예시 (app/api/auth/callback/route.ts)
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(req: Request) {
  const sessionId = '...'
  const isProd = process.env.NODE_ENV === 'production'

  cookies().set('session', sessionId, {
    httpOnly: true,
    secure: isProd,
    sameSite: isProd ? 'none' : 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7,
  })

  return NextResponse.redirect(new URL('/', req.url))
}

만약 여기서도 루프가 난다면, DevTools에서 실제로 Set-Cookie가 내려오는지부터 확인하세요. “코드는 쿠키를 세팅했는데 헤더가 없다”면 배포 플랫폼/프록시가 헤더를 변조하는 문제일 수 있습니다.

해결 3: 프록시 환경에서 Secure 누락 문제 잡기

로드밸런서 뒤에서 앱이 HTTP로 받으면, 라이브러리가 요청 스킴을 HTTP로 인식해 Secure 쿠키를 빼는 일이 흔합니다. Express라면 아래 설정이 대표적입니다.

// Express behind proxy
app.set('trust proxy', 1)

그리고 로드밸런서가 X-Forwarded-Proto를 전달하는지 확인하세요. 전달이 안 되면 앱은 HTTPS 요청을 HTTPS로 인지하지 못합니다.

Kubernetes나 클라우드 환경에서 네트워크가 의심된다면, egress/라우팅 문제와는 결이 다르지만 “프록시/게이트웨이 레이어에서 헤더가 어떻게 변형되는지”를 함께 점검하는 습관이 도움이 됩니다. 인프라 트러블슈팅 관점은 EKS에서 Pod는 정상인데 egress만 막힐 때 점검 같은 글도 참고할 만합니다.

해결 4: 도메인 전략 정리(가장 단순한 구성이 가장 강함)

가능하면 아래처럼 단순화하는 것이 쿠키 이슈를 크게 줄입니다.

  • 앱과 콜백을 동일 오리진으로 통일: https://app.example.com/api/auth/callback
  • API도 같은 오리진 프록시로 통일: https://app.example.com/api/*

서브도메인을 분리해야 한다면, 쿠키 Domain을 상위로 올릴지, 아니면 API 호출을 BFF 패턴으로 “같은 오리진에서만” 하도록 바꿀지 결정해야 합니다.

  • 프론트에서 api.example.com 직접 호출: 쿠키 스코프/CSRF/CORS 복잡도 증가
  • 프론트는 app.example.com만 호출하고, 서버가 내부에서 api.example.com 호출: 브라우저 쿠키 이슈 감소

해결 5: OAuth 상태값(state)과 PKCE 검증 실패가 루프처럼 보이는 경우

쿠키 문제처럼 보이지만 실제로는 state 또는 PKCE code_verifier 저장 실패로 인해 콜백 검증이 실패하고, 앱이 다시 로그인으로 보내는 경우도 있습니다.

이때도 원인은 비슷합니다.

  • state를 쿠키에 저장했는데 쿠키가 차단됨
  • 혹은 세션 저장소에 써야 하는데 저장 실패

따라서 콜백에서 다음을 로깅하면 진단이 빨라집니다.

  • 수신한 state 값 존재 여부
  • 서버가 기대한 state 값 존재 여부
  • PKCE code_verifier 조회 성공 여부

단, 민감정보는 마스킹하거나 해시로 남기세요.

// 예시: state 비교 로깅(민감정보 직접 출력 금지)
console.log('oauth callback state present:', Boolean(req.query.state))
console.log('expected state present:', Boolean(req.session?.oauthState))

운영에서 재발 방지: 관측 포인트와 테스트 케이스

관측(로그/메트릭)

  • 콜백 처리 성공률(토큰 교환 성공)
  • 콜백 직후 첫 요청에서의 인증 성공률
  • Set-Cookie 포함 응답 비율(가능하면 엣지/프록시에서도 확인)
  • 사용자 에이전트별 실패율(Chrome만 높으면 쿠키 정책 가능성 상승)

테스트 케이스

  • Chrome 시크릿 모드(확장프로그램 영향 배제)
  • 서드파티 쿠키 차단 옵션 변경 후 동작 비교
  • https 운영과 동일한 조건(가능하면 스테이징에서 동일 도메인/동일 TLS)
  • 서로 다른 서브도메인 조합에서 쿠키 전달 확인

결론

Chrome에서 OAuth 로그인 루프가 발생할 때는 “OAuth 자체”보다 “세션 쿠키가 저장·전송되는가”를 먼저 의심하는 것이 가장 빠른 길입니다. SameSite=NoneSecure의 짝, 올바른 DomainPath, 프록시 뒤 스킴 인식(X-Forwarded-Prototrust proxy)만 제대로 맞춰도 대부분의 루프는 사라집니다.

추가로 Next.js 환경에서의 리다이렉트 루프 패턴과 미들웨어/라우팅 충돌까지 함께 보고 싶다면 Next.js OAuth 로그인 무한 리다이렉트 해결 가이드도 같이 확인해 보세요.