- Published on
Nginx 뒤 OAuth 콜백 302 무한리다이렉트 원인
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 OAuth 로그인(Authorization Code Flow)을 붙인 서비스가 Nginx 뒤로 들어가는 순간, 잘 되던 콜백이 302를 반복하며 로그인 루프에 빠지는 일이 자주 발생합니다. 겉으로는 “로그인 버튼 → IdP(구글/깃허브/카카오) → 우리 서비스 콜백 → 다시 로그인 페이지”가 계속 반복됩니다.
이 글은 Nginx 뒤에서 OAuth 콜백이 302 무한 리다이렉트로 이어지는 전형적인 원인을 “요청 스킴/호스트 인식”과 “세션 쿠키 저장 실패” 관점에서 분해해 설명하고, Nginx·애플리케이션(특히 Spring/Node/Next.js)에서의 해결책을 코드로 정리합니다.
> 프론트(Next.js) 쪽에서 루프가 나는 케이스는 Next.js OAuth 로그인 무한 리다이렉트 해결 가이드도 함께 보면 원인 범위를 빠르게 좁힐 수 있습니다.
증상 패턴: 302가 “정상”처럼 보이는데 왜 무한 루프가 되나
OAuth의 코드 플로우는 본질적으로 리다이렉트를 여러 번 사용합니다.
/login→ IdP로 302- IdP 인증 후 →
/oauth/callback?code=...&state=...로 302 - 콜백에서 코드 교환 후 →
/또는/app으로 302
문제는 2 또는 3 단계에서 세션이 유지되지 않거나, 앱이 URL을 잘못 생성하면 앱이 “아직 로그인 안 됨”으로 판단해 다시 1로 보내면서 루프가 시작된다는 점입니다.
루프의 핵심 원인은 보통 아래 두 축 중 하나입니다.
- 앱이 외부에서 들어온 요청을
http로 오인(실제는 https) → redirect_uri/absolute URL 생성이 틀어짐 - Set-Cookie가 브라우저에 저장되지 않음(Secure/SameSite/Domain/Path 문제) → 콜백 처리 후에도 세션이 없다고 판단
원인 1) X-Forwarded-Proto/Host 누락으로 스킴이 http로 인식됨
Nginx가 TLS를 종료하고(https는 Nginx에서 끝남) 백엔드로는 http로 프록시하는 구조에서, 백엔드는 기본적으로 요청 스킴을 http로 봅니다.
그 결과:
- 백엔드가
redirect_uri=http://example.com/oauth/callback같은 값을 생성 - IdP에 등록된 redirect_uri는
https://example.com/oauth/callback - 혹은 콜백 이후 앱이
http://...로 302 → 브라우저/보안정책/프레임워크에서 다시 https로 올리거나, 다시 인증 플로우를 타면서 루프
Nginx에서 반드시 전달해야 하는 헤더
아래는 “외부 클라이언트가 보던 원본 정보”를 백엔드가 알 수 있게 하는 최소 세트입니다.
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
$scheme는 Nginx에 들어온 요청 기준입니다. 즉 https로 들어오면https가 됩니다.Host가 내부 호스트로 바뀌면(예: upstream 이름) OAuth redirect URI 생성이 틀어지기 쉽습니다.
Spring Boot에서 프록시 헤더 신뢰 설정
Spring 계열은 프록시 환경에서 헤더를 신뢰하도록 설정해야 “외부 스킴/호스트”를 올바르게 복원합니다.
application.yml
server:
forward-headers-strategy: framework
또는 환경에 따라(특히 오래된 구성/서블릿 컨테이너) 아래가 필요할 수 있습니다.
server:
tomcat:
remoteip:
protocol-header: X-Forwarded-Proto
remote-ip-header: X-Forwarded-For
이 설정이 없으면, 컨트롤러에서 request.getScheme()가 http로 찍히고, Spring Security가 생성하는 redirect URL도 어긋날 수 있습니다.
> Cloudflare/다단 프록시에서 특히 http로 떨어지는 증상은 Cloudflare 뒤에서 OAuth 콜백이 http로 떨어질 때와 원인이 거의 같습니다. Nginx 앞단에 CDN/ALB가 하나 더 있으면 “어느 레이어에서 X-Forwarded-Proto가 깨지는지”부터 확인하세요.
원인 2) redirect_uri(또는 callback URL) 불일치
IdP는 보안상 redirect_uri의 정확한 일치(스킴/호스트/포트/패스)를 요구합니다. 프록시 뒤에서 아래처럼 미세하게 달라지면, 에러가 나거나(명확한 에러) 혹은 앱이 자체적으로 다시 로그인으로 보내며(불명확한 루프) 문제가 커집니다.
https://example.com/oauth/callback(등록값)https://example.com/oauth/callback/(슬래시 하나 차이)https://www.example.com/oauth/callbackvshttps://example.com/oauth/callbackhttps://example.com:443/oauth/callbackvs 포트 생략- 내부에서
http://app:8080/oauth/callback로 생성
진단 체크리스트
- 브라우저 DevTools Network에서 IdP로 나가는 authorize 요청의 redirect_uri를 확인
- 콜백으로 돌아왔을 때 서버 로그에 찍히는 Host/Proto가 기대값인지 확인
- 애플리케이션이 “절대 URL”을 만들어내는 지점(프레임워크 설정/ENV)을 확인
원인 3) 세션 쿠키가 저장되지 않아 “로그인한 적이 없음”으로 반복
콜백에서 코드 교환은 성공했는데도 다시 로그인으로 튕긴다면, 대부분 세션 쿠키가 브라우저에 저장되지 않거나 다음 요청에 실리지 않기 때문입니다.
대표적인 원인:
Set-Cookie에Secure가 필요한데 http로 인식되어 빠짐SameSite정책 때문에 OAuth 리다이렉트 흐름에서 쿠키가 제외됨Domain이 잘못되어 쿠키가 다른 도메인에만 저장됨Path가 너무 좁아/oauth/callback에만 붙고/에는 안 붙음
SameSite/secure 기본 규칙 (현업에서 자주 밟는 지점)
- 최신 브라우저는 기본적으로
SameSite=Lax에 가깝게 동작하며, 크로스사이트 POST/iframe 등에서 쿠키가 누락될 수 있습니다. - OAuth는 “외부(IdP) → 우리 도메인”으로 돌아오는 크로스사이트 내비게이션이 포함되므로, 구현 방식에 따라
SameSite=None; Secure가 필요합니다.
예: Express/Node 세션 쿠키 설정
app.set('trust proxy', 1); // Nginx 뒤면 필수(헤더 신뢰)
app.use(session({
name: 'sid',
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // https에서만
sameSite: 'none', // OAuth/크로스사이트 흐름이면 필요할 수 있음
path: '/',
}
}));
여기서 trust proxy를 켜지 않으면, Express는 요청을 http로 판단해 secure: true 쿠키를 아예 세팅하지 않거나, 동작이 꼬여 “로그인 성공 → 다음 요청에 세션 없음” 루프가 납니다.
예: Spring Security에서 쿠키/세션이 꼬일 때
Spring Security 자체 세션(JSESSIONID)을 쓰든, 별도 쿠키를 쓰든 원리는 같습니다.
X-Forwarded-Proto신뢰가 안 되면 Secure 쿠키가 기대대로 동작하지 않거나, 리다이렉트 URL이 http로 생성될 수 있습니다.SameSite는 기본값이 환경마다 달라, 프론트/백 분리 + 크로스 도메인 조합에서 문제가 됩니다.
Spring Boot 3 기준으로는 server.forward-headers-strategy 설정과 함께, 필요 시 SameSite를 명시적으로 다루는 구성이 필요합니다(프레임워크/서버 조합에 따라 접근이 다름).
원인 4) Nginx의 redirect 재작성(proxy_redirect)로 Location이 변형됨
백엔드가 Location: http://app:8080/... 같은 값을 내려주면, Nginx가 이를 외부 도메인으로 바꿔줘야 합니다. 반대로, 이미 올바른 Location인데 Nginx가 잘못 재작성하면 루프가 생깁니다.
Nginx 설정 포인트
location / {
proxy_pass http://app_upstream;
# 기본적으로 on인 경우가 많지만, 환경에 따라 명시
proxy_redirect off;
}
- 백엔드가 항상 상대경로(
/app)로만 리다이렉트하도록 만들면 이 계열 문제는 크게 줄어듭니다. - 어쩔 수 없이 절대 URL을 쓰면,
proxy_redirect규칙을 정확히 잡아야 합니다.
원인 5) state/nonce 검증 실패 → 앱이 재인증으로 회귀
OAuth/OIDC는 CSRF 방지를 위해 state(OIDC는 nonce)를 검증합니다. 이 값은 보통 “세션”이나 “임시 쿠키”에 저장됩니다.
- 콜백에서 state가 맞지 않으면 → 인증 실패
- 많은 앱은 실패 시
/login으로 302 - 결과적으로 “콜백 → 실패 → 로그인” 루프
이 경우는 서버 로그에 state mismatch/invalid state가 찍히는 경우가 많습니다. 원인은 결국 위에서 말한 쿠키/세션 저장 실패(특히 SameSite/secure)로 귀결되는 일이 흔합니다.
10분 안에 끝내는 진단 순서(실전)
아래 순서대로 보면, 대부분 10분 내에 원인 후보가 1~2개로 줄어듭니다.
- DevTools Network에서 루프를 발생시키는 302 체인을 펼친다.
- 각 302 응답의 Location이
http로 떨어지는지,www가 붙는지, 포트가 붙는지 확인한다. - 콜백 응답에서 Set-Cookie가 내려오는지, 내려오는데도 다음 요청에 Cookie가 실리지 않는지 확인한다.
- Nginx access log에
$scheme $host $request_uri $upstream_http_location를 임시로 찍어 “어디서 http가 섞이는지”를 찾는다.
예: Nginx 로그 포맷(임시)
log_format oauth_debug '$remote_addr $scheme $host "$request" '
'status=$status location="$sent_http_location" '
'up_location="$upstream_http_location" '
'xfp="$http_x_forwarded_proto"';
access_log /var/log/nginx/access.log oauth_debug;
권장 기준 구성: Nginx + 앱 공통 베스트 프랙티스
1) Nginx는 원본 정보를 온전히 전달
server {
listen 443 ssl;
server_name example.com;
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
}
}
2) 앱은 프록시 헤더를 신뢰하고(특히 Node/Next), 외부 base URL을 일관되게 사용
- Node/Express:
trust proxy - Spring:
server.forward-headers-strategy=framework - NextAuth/Next.js:
NEXTAUTH_URL,trustHost(버전별) 등 “외부 URL”을 명시
3) 쿠키 정책을 OAuth 흐름에 맞춘다
- 크로스사이트 리다이렉트에서 쿠키가 필요하면
SameSite=None; Secure - 도메인/서브도메인 전략을 명확히(예:
auth.example.com과app.example.com분리 시 특히)
마무리: 루프의 본질은 ‘외부에서 보이는 URL’과 ‘서버가 믿는 URL’의 불일치
Nginx 뒤 OAuth 콜백 302 무한리다이렉트는 복잡해 보이지만, 대부분 아래 둘 중 하나로 정리됩니다.
- 서버가 요청을 http/내부 호스트로 오인해서 redirect_uri/Location을 잘못 만든다 → X-Forwarded-* + 프레임워크 신뢰 설정
- 쿠키가 저장/전달되지 않아 state/세션이 유지되지 않는다 → Secure/SameSite/Domain/Path + 프록시 신뢰
위의 체크리스트와 설정을 적용해도 해결이 안 된다면, “어느 레이어에서 최초로 http가 섞이는지(예: ALB/Cloudflare/Nginx)”를 먼저 확정하고, 그 레이어에서 X-Forwarded-Proto/Forwarded 표준 헤더가 어떻게 변형되는지부터 역추적하는 방식이 가장 빠릅니다.