- Published on
Proxy 뒤 Nginx에서 OAuth 리다이렉트 URI 불일치 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버를 Nginx로 띄워두고 그 앞단에 ALB/NLB, Cloudflare, API Gateway, 사내 L7 프록시 같은 리버스 프록시가 붙는 순간 OAuth 로그인에서 자주 터지는 문제가 있습니다. 바로 리다이렉트 URI 불일치(redirect_uri mismatch) 입니다.
증상은 대개 다음 중 하나로 나타납니다.
- IdP(Google, GitHub, Keycloak, Cognito 등)에서
redirect_uri_mismatch/invalid_redirect_uri에러 - 애플리케이션이 생성한 콜백 URL이
http://로 내려가거나 포트가 붙음 - 외부 도메인(
app.example.com)으로 접속했는데 내부 호스트명(nginx:8080)이 redirect_uri에 섞임
이 글은 "Nginx behind proxy" 환경에서 왜 이런 현상이 생기는지, 그리고 Nginx + 애플리케이션(예: Spring Security, Node/Express)에서 무엇을 어떻게 고쳐야 하는지를 실전 관점에서 정리합니다.
문제의 본질: OAuth가 요구하는 "정확히 일치" 조건
OAuth/OIDC에서 redirect_uri는 보안상 이유로 사전 등록된 값과 완전히 일치해야 합니다. 즉,
- 스킴(
httpvshttps) - 호스트
- 포트
- 경로
- (일부 IdP는 쿼리까지)
가 1글자라도 다르면 거절됩니다.
그런데 프록시 뒤에 있는 애플리케이션은 종종 자기가 받은 요청을 "내부 요청" 기준으로 해석합니다.
- 프록시가 외부 HTTPS를 내부 HTTP로 터미네이션
- 프록시가 Host 헤더를 바꾸거나, 내부 서비스명으로 라우팅
- 애플리케이션이
X-Forwarded-Proto,X-Forwarded-Host를 신뢰하지 않음
결과적으로 애플리케이션이 만들어내는 redirect_uri가 외부 사용자가 보는 URL과 달라집니다.
빠른 진단: 실제로 어떤 URL이 생성되는지 확인
가장 먼저 해야 할 일은 “IdP로 보내는 authorize 요청에 어떤 redirect_uri가 들어갔는지”를 확인하는 것입니다.
- 브라우저 개발자도구(Network)에서
/authorize요청 확인 - 서버 로그에서 authorization request 로그 확인
- Nginx access log에
$request_uri및 forwarded 헤더를 임시로 출력
Nginx에서 forwarded 헤더를 로그로 찍어보기
log_format with_forwarded '$remote_addr - $host "$request" '
'xfh="$http_x_forwarded_host" xfp="$http_x_forwarded_proto" '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log with_forwarded;
여기서 xfp가 비어 있거나 http로 찍히면, 애플리케이션이 스킴을 잘못 추론할 가능성이 높습니다.
원인 1: 프록시가 주는 X-Forwarded-*를 Nginx가 전달하지 않음
프록시(예: ALB)가 X-Forwarded-Proto: https를 넣어줬는데, Nginx가 upstream으로 전달하지 않으면 앱은 여전히 http로 판단합니다.
권장 Nginx 설정(표준 헤더 전달)
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app_upstream;
# 원본 Host 유지
proxy_set_header Host $host;
# 클라이언트 IP 체인
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;
# (선택) 표준 Forwarded 헤더도 함께
proxy_set_header Forwarded "for=$proxy_add_x_forwarded_for;proto=$scheme;host=$host";
}
}
중요 포인트:
proxy_set_header Host $host;를 누락하면 upstream이 내부 호스트명으로 redirect_uri를 만들 수 있습니다.$scheme은 Nginx가 받은 스킴입니다. 만약 Nginx 앞단에서 TLS가 종료되고 Nginx는 HTTP만 받는다면$scheme은 항상http가 됩니다. 이 경우는 다음 원인(원인 2)로 넘어가야 합니다.
원인 2: TLS가 앞단에서 종료되어 Nginx는 항상 http로 인식
가장 흔한 케이스입니다.
- 외부:
https://app.example.com - 프록시(예: ALB)에서 TLS 종료
- 내부:
http://nginx:80로 전달
이때 Nginx의 $scheme은 http이므로 위 설정을 그대로 쓰면 X-Forwarded-Proto: http가 앱으로 전달됩니다.
해결은 “앞단 프록시가 준 X-Forwarded-Proto를 신뢰”하도록 Nginx를 구성하는 것입니다.
앞단의 X-Forwarded-Proto를 그대로 전달
map $http_x_forwarded_proto $proxy_xfp {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 핵심: 앞단에서 https라고 알려주면 그대로 전달
proxy_set_header X-Forwarded-Proto $proxy_xfp;
proxy_set_header X-Forwarded-Host $host;
# 필요 시 포트도 보정
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
}
}
X-Forwarded-Port는 프록시가 안 주는 경우가 많아, 앱이 포트를 따로 쓰지 않는다면 생략해도 됩니다.- Cloudflare 같은 CDN은
CF-Visitor: {"scheme":"https"}로 주기도 하므로 환경에 따라 매핑이 필요할 수 있습니다.
원인 3: 애플리케이션이 forwarded 헤더를 신뢰하지 않음
Nginx가 올바른 X-Forwarded-Proto/Host를 전달했는데도 redirect_uri가 계속 틀리면, 앱이 “프록시 헤더를 무시”하고 있을 가능성이 큽니다.
Spring Boot / Spring Security OAuth2의 경우
Spring은 기본적으로 보안상 Forwarded 헤더를 무조건 신뢰하지 않습니다(특히 프록시 체인이 있는 환경).
(권장) Spring Boot에서 forward-headers 전략 설정
application.yml
server:
forward-headers-strategy: framework
또는(레거시/환경에 따라)
server:
use-forward-headers: true
그리고 프록시가 X-Forwarded-*를 주는지/정확한지 확인합니다.
추가로, Spring Security에서 OIDC/JWKS 관련 이슈가 함께 발생하는 경우도 있는데(예: kid 불일치, 캐시 문제), 그건 별개의 축입니다. 토큰 검증 단계에서 401이 난다면 아래 글도 함께 참고하면 디버깅 동선이 줄어듭니다.
Node.js (Express) + Passport/OAuth 라이브러리의 경우
Express는 프록시 뒤에서 원본 스킴을 알려면 trust proxy 설정이 필요합니다.
import express from 'express';
const app = express();
// 1) 단일 프록시(nginx) 뒤라면
app.set('trust proxy', 1);
// 2) 사내망/특정 대역만 신뢰하려면
// app.set('trust proxy', 'loopback, 10.0.0.0/8');
app.get('/debug', (req, res) => {
res.json({
protocol: req.protocol,
host: req.get('host'),
originalUrl: req.originalUrl,
xfp: req.get('x-forwarded-proto'),
xfh: req.get('x-forwarded-host')
});
});
req.protocol이 여전히 http면, (1) forwarded 헤더가 안 오거나 (2) trust proxy가 제대로 적용되지 않은 것입니다.
원인 4: Nginx의 redirect/rewrite가 Location 헤더를 망가뜨림
OAuth 플로우 중에는 302 리다이렉트가 많습니다. 이때 Nginx가 upstream의 Location 헤더를 재작성하면서 스킴/호스트를 바꿔버릴 수 있습니다.
proxy_redirect기본 동작- upstream이
Location: http://internal:8080/...같은 값을 내보내는 경우
proxy_redirect 점검/보정
가장 안전한 쪽은 upstream이 올바른 외부 URL을 만들도록(Forwarded 헤더 신뢰) 고치고, Nginx는 불필요한 재작성을 최소화하는 것입니다.
location / {
proxy_pass http://app_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $proxy_xfp;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 필요 없으면 끄기
proxy_redirect off;
}
만약 upstream이 내부 호스트로 Location을 내보내는 것을 당장 고칠 수 없다면, 임시로 매핑할 수도 있습니다.
proxy_redirect http://internal:8080/ https://app.example.com/;
다만 이 방식은 환경이 늘어나면 유지보수가 급격히 어려워지므로 “임시 처방”으로만 권장합니다.
체크리스트: redirect_uri mismatch를 끝내는 7가지 확인
운영에서 재현이 어려워서 삽질이 길어지는 케이스가 많아, 체크리스트 형태로 정리합니다.
- IdP에 등록된 Redirect URI가 정확한가? (스킴/호스트/경로)
- 브라우저에서 authorize 요청의
redirect_uri가 무엇으로 나가는가? - 프록시가
X-Forwarded-Proto: https를 넣어주는가? - Nginx가 그 헤더를 upstream으로 전달하는가?
- 앱이 forwarded 헤더를 신뢰하도록 설정되어 있는가? (Spring
forward-headers-strategy, Expresstrust proxy) - Nginx가
proxy_redirect로 Location을 재작성하고 있지 않은가? - 멀티 프록시 체인이라면(Cloudflare → ALB → Nginx) 어느 지점의 헤더가 최종 진실인지 정했는가?
실전 예시: ALB(HTTPS) → Nginx(HTTP) → Spring Boot
가장 흔한 구성을 예로 “정답 세트”를 한 번에 제시하면 아래와 같습니다.
1) Nginx
map $http_x_forwarded_proto $proxy_xfp {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://spring:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_xfp;
proxy_set_header X-Forwarded-Host $host;
proxy_redirect off;
}
}
2) Spring Boot
server:
forward-headers-strategy: framework
이 조합에서 대부분의 redirect_uri mismatch는 정리됩니다.
부록: 장애 대응 관점에서 함께 보면 좋은 글
리버스 프록시 환경에서는 OAuth 말고도 “요청 크기 제한” 같은 Nginx 레벨 이슈가 같이 터지곤 합니다(특히 로그인 콜백에 쿠키/헤더가 커지는 환경). 업로드/요청이 413으로 실패한다면 아래 글이 바로 도움이 됩니다.
또한 Kubernetes Ingress/Nginx 뒤에서 설정을 바꿨는데도 반영이 안 되거나, 특정 요청만 크게 실패하는 경우는 인그레스/게이트웨이 레이어까지 함께 봐야 합니다. API 게이트웨이/Ingress에서 413이 난다면 아래도 참고할 만합니다.
마무리
OAuth 리다이렉트 URI 불일치는 “OAuth 설정을 잘못했다”기보다, 대부분 프록시 체인에서 원본 URL(스킴/호스트)을 잃어버린 문제입니다.
해결의 핵심은 단순합니다.
- 프록시가 원본 정보를
X-Forwarded-*로 제공하고 - Nginx가 그것을 upstream으로 정확히 전달하며
- 애플리케이션이 그 헤더를 신뢰하도록 설정하고
- 불필요한
proxy_redirect재작성으로 Location을 망치지 않는다
이 4가지만 맞추면, 환경이 ALB든 Cloudflare든 Kubernetes Ingress든 redirect_uri mismatch는 재발 확률이 크게 줄어듭니다.