Published on

Nginx 뒤 OAuth2 리다이렉트 루프 5분 해결

Authors

서버를 Nginx 뒤에 두고 OAuth2 로그인(Authorization Code Flow)을 붙였는데, 로그인 성공처럼 보이다가 다시 로그인 화면으로 튕기고, 브라우저 네트워크 탭에는 302가 계속 반복되는 상황이 자주 나옵니다. 이 문제는 대개 앱은 로그인됐다고 생각하지만 브라우저가 세션 쿠키를 저장하지 못하거나, 혹은 리다이렉트 URL이 프록시 환경에서 다르게 계산되어 인증 서버와 앱이 서로 다른 세계를 바라볼 때 발생합니다.

이 글은 “5분 해결”을 목표로, 가장 흔한 원인을 체크리스트 순서대로 잡아내고, Nginx와 애플리케이션 설정을 한 번에 정리합니다.

증상 패턴을 먼저 확정하기

리다이렉트 루프는 보통 아래 패턴 중 하나입니다.

  1. GET / 302 Location: /oauth2/authorization/... 같은 로그인 시작 URL로 이동
  2. 인증 서버 로그인 완료 후 GET /login/oauth2/code/... 콜백으로 돌아옴
  3. 앱이 Set-Cookie로 세션을 내려줌
  4. 다음 요청에서 세션 쿠키가 안 붙어서 다시 1로 돌아감

즉, 핵심은 콜백 이후 세션이 유지되는지입니다.

1분 진단: 브라우저에서 쿠키가 실제로 저장되나

Chrome DevTools 기준으로:

  • Network 탭에서 콜백 응답(예: /callback, /login/oauth2/code/...)을 클릭
  • Response Headers에 Set-Cookie가 있는지 확인
  • Application 탭 Cookies에서 해당 쿠키가 저장됐는지 확인

여기서 쿠키가 저장되지 않으면 거의 항상 아래 중 하나입니다.

  • Secure 쿠키인데 브라우저가 http로 인식
  • SameSite 정책에 의해 차단
  • Domain/Path가 현재 호스트와 불일치
  • 프록시가 Set-Cookie를 훼손하거나, 앱이 잘못된 외부 URL로 리다이렉트

원인 1: X-Forwarded-Proto 미설정으로 스킴이 뒤집힘

Nginx는 외부에서 https로 받고 내부 업스트림에는 http로 전달하는 구성이 흔합니다. 그런데 앱이 프록시 헤더를 신뢰하지 않으면, 자기가 http로 서비스 중이라고 착각합니다.

그 결과:

  • 앱이 http 기준으로 리다이렉트 URL을 만들거나
  • 세션 쿠키를 Secure로 내려야 하는데 조건이 맞지 않아 내려주지 않거나
  • 인증 서버에 등록된 redirect URI(https)와 앱이 생성한 redirect URI(http)가 달라져 반복 시도

해결: Nginx에 Forwarded 헤더를 정확히 전달

아래는 가장 기본이면서도 효과가 큰 설정입니다.

server {
  listen 443 ssl;
  server_name app.example.com;

  location / {
    proxy_pass http://app_upstream;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # 필요 시 WebSocket 등
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }
}

중요 포인트는 X-Forwarded-Proto가 반드시 외부 스킴(대개 https)을 반영해야 한다는 점입니다.

앱도 프록시 헤더를 신뢰하도록 설정해야 함

프레임워크별로 “프록시 신뢰” 옵션이 따로 있습니다.

  • Spring Boot: server.forward-headers-strategy=framework 또는 환경에 맞는 설정
  • Express: app.set('trust proxy', 1)
  • Django: SECURE_PROXY_SSL_HEADER 설정

앱이 이 헤더를 무시하면 Nginx 설정만으로는 반쪽 해결이 됩니다.

원인 2: OAuth2 콜백 URL이 외부 URL과 불일치

인증 서버에는 redirect URI가 등록돼 있고, 앱은 런타임에 redirect URI를 구성합니다. 프록시 뒤에서는 이 값이 흔히 어긋납니다.

대표 케이스:

  • 인증 서버 등록: https://app.example.com/oauth/callback
  • 앱이 생성: http://app:8080/oauth/callback 또는 http://app.example.com/oauth/callback

이러면 로그인 후 돌아오긴 하지만, 다음 단계에서 다시 로그인으로 돌아가거나, 인증 서버가 다시 인증을 요구하면서 루프처럼 보입니다.

해결: 외부 기준(base URL) 명시

프레임워크에 따라 다음 중 하나로 해결합니다.

  • 외부 URL을 명시하는 설정(예: PUBLIC_URL, BASE_URL)
  • 프록시 헤더 신뢰 설정을 켠 후, redirect URI 구성 로직이 헤더를 반영하도록 수정

Nginx 레벨에서 호스트/스킴을 정확히 전달(Host, X-Forwarded-Proto)하는 것이 선행 조건입니다.

원인 3: SameSite, Secure 쿠키 정책으로 세션이 버려짐

OAuth2는 보통 “인증 서버 도메인”에서 “앱 도메인”으로 돌아오는 교차 사이트 흐름이 포함됩니다. 이때 쿠키 정책이 맞지 않으면 브라우저가 쿠키를 저장하지 않거나, 다음 요청에 쿠키를 붙이지 않습니다.

빠른 체크

  • Set-CookieSameSite=Strict가 붙어 있으면 콜백 이후 동작이 꼬일 수 있습니다.
  • SameSite=None을 쓰려면 반드시 Secure가 필요합니다.
  • 그런데 브라우저가 현재 연결을 http로 인식하면 Secure 쿠키는 저장되지 않습니다.

즉, 이 이슈는 종종 “원인 1의 스킴 뒤집힘”과 같이 나타납니다.

Nginx에서 쿠키 속성 보정(필요한 경우)

애플리케이션 수정이 어렵다면 Nginx에서 쿠키 속성을 리라이트할 수도 있습니다.

location / {
  proxy_pass http://app_upstream;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;

  # 업스트림이 Set-Cookie를 덜 엄격하게 주는 경우 보정
  proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
}

주의:

  • 모든 쿠키에 일괄 적용되므로 영향 범위를 확인해야 합니다.
  • 가능한 한 애플리케이션에서 세션 쿠키 정책을 올바르게 설정하는 것이 정석입니다.

원인 4: 프록시 리다이렉트가 내부 호스트로 새는 문제

업스트림이 Location: http://app:8080/... 같은 내부 주소로 리다이렉트를 내보내면, 브라우저는 접근할 수 없거나, 다시 Nginx로 돌아오면서 루프가 생깁니다.

해결: proxy_redirect 점검

기본적으로 Nginx는 일부 Location을 수정하지만, 환경에 따라 명시가 필요합니다.

location / {
  proxy_pass http://app_upstream;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;

  # 내부 리다이렉트를 외부 도메인으로 교정
  proxy_redirect http://app_upstream/ https://app.example.com/;
}

또는 업스트림이 호스트 기반으로 리다이렉트를 만들도록 Host 헤더를 정확히 전달하는 것만으로 해결되기도 합니다.

5분 해결 체크리스트(이 순서대로)

  1. 브라우저에서 콜백 응답의 Set-Cookie 확인
    • 저장이 안 되면 Secure/SameSite/스킴 문제 가능성 큼
  2. Nginx에 proxy_set_header X-Forwarded-Proto $scheme; 추가
  3. 앱에서 “프록시 헤더 신뢰” 옵션 활성화
  4. 인증 서버에 등록된 redirect URI와 실제 외부 콜백 URL이 완전히 동일한지 확인
    • 스킴, 호스트, 포트, 경로, 트레일링 슬래시까지
  5. 내부 호스트로 리다이렉트가 새면 proxy_redirect 또는 앱의 base URL 설정

이 다섯 단계로 대부분의 리다이렉트 루프는 끊깁니다.

재현과 확인을 위한 curl 로그 패턴

브라우저가 아니라 서버에서 빠르게 확인하려면 curl로 리다이렉트를 따라가며 헤더를 보는 방법이 좋습니다.

curl -I -L https://app.example.com/ \
  -c cookies.txt -b cookies.txt

관찰 포인트:

  • Set-Cookie가 내려오는지
  • 다음 요청에서 Cookie:가 붙는지
  • Location:http로 떨어지거나 내부 호스트로 떨어지지 않는지

운영 환경에서 자주 같이 터지는 Nginx 이슈들

OAuth2 루프를 잡다 보면 “프록시 뒤에서만 재현되는” 다른 문제도 함께 발견됩니다. 특히 업로드나 스트리밍과 엮이면 Nginx 기본값 때문에 증상이 더 복잡해지기도 합니다.

결론: 루프의 본질은 “외부 세계를 앱이 모르고 있다”

Nginx 뒤 OAuth2 리다이렉트 루프는 복잡해 보이지만, 본질은 단순합니다.

  • 앱이 자기 자신을 http로 착각하거나
  • 외부 호스트를 모르거나
  • 그 결과 세션 쿠키가 유지되지 않거나
  • redirect URI가 불일치한다

가장 먼저 X-Forwarded-ProtoHost 전달, 그리고 앱의 프록시 신뢰 설정을 맞추면, 5분 안에 루프가 끊기는 경우가 많습니다. 이후에도 남는 문제는 쿠키 정책(SameSite, Secure)과 redirect URI 정합성을 순서대로 좁혀가면 안정적으로 해결됩니다.