- Published on
Nginx 뒤 OAuth 콜백 400, redirect_uri 불일치 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 환경에서 OAuth 로그인을 붙일 때 로컬에서는 잘 되는데, Nginx 뒤에 올리자마자 콜백이 400으로 떨어지는 경우가 많습니다. 특히 에러 메시지에 redirect_uri 불일치가 보이거나, IdP(구글/카카오/깃허브/Keycloak 등) 쪽에서 redirect_uri_mismatch, invalid_request 같은 응답을 주면 거의 확실히 프록시 뒤에서 애플리케이션이 “자기 주소”를 잘못 인식하고 있습니다.
이 글에서는 Nginx 리버스 프록시 환경에서 OAuth 콜백 400(redirect URI mismatch)의 원인을 체계적으로 분해하고, Nginx와 애플리케이션(Spring Boot/Spring Security 중심)에서의 실전 해결책을 코드와 설정으로 정리합니다.
증상: 로컬 OK, Nginx 뒤 400
대표 증상은 아래 중 하나로 나타납니다.
- IdP 로그인 후 콜백 단계에서 400
- IdP 콘솔/로그에
redirect_uri불일치 기록 - 애플리케이션 로그에
authorization_request_not_found,invalid_state_parameter,Invalid redirect_uri등 - HTTPS로 서비스 중인데 서버가 콜백을 HTTP로 생성(또는 반대)
- 포트가 포함되거나(
:8080) 누락되어 불일치 - 경로가
/login/oauth2/code/...인데 프록시 prefix 때문에/api/login/oauth2/code/...로 바뀌어 불일치
핵심은 IdP에 등록된 redirect URI와 실제 요청/서버가 생성한 redirect URI가 바이트 단위로 같아야 한다는 점입니다(스킴, 호스트, 포트, path, trailing slash까지).
원인 1: X-Forwarded-* 미전달로 스킴/호스트 오인
Nginx 뒤에서 애플리케이션은 보통 내부 통신을 HTTP로 받습니다. 그런데 외부는 HTTPS라면, 애플리케이션이 “나는 HTTP로 접근받았다”고 오해하고 redirect URI를 http://... 로 생성해버립니다.
Nginx에서 반드시 전달할 헤더
아래는 가장 기본이면서도 효과가 큰 설정입니다.
location / {
proxy_pass http://app:8080;
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;
}
Host가 바뀌면 redirect URI 호스트가 달라집니다.X-Forwarded-Proto가 없으면 HTTPS를 HTTP로 오인합니다.X-Forwarded-Port가 없거나 잘못되면:80/:443처리에서 꼬일 수 있습니다.
원인 2: Spring Boot가 Forwarded 헤더를 신뢰하지 않음
Nginx가 헤더를 줘도, 애플리케이션이 이를 “신뢰”해서 request URL을 재구성하지 않으면 동일 문제가 납니다.
Spring Boot 설정: forward headers 전략
Spring Boot 3 기준으로는 아래 설정이 흔히 필요합니다.
server:
forward-headers-strategy: native
환경에 따라 framework 가 더 적절할 때도 있습니다. 중요한 건 한 번 정하고 일관되게 운영하는 것입니다.
native: 서블릿 컨테이너/인프라가 제공하는 forwarded 처리를 신뢰framework: 스프링 프레임워크 레벨에서 처리
문제가 계속되면 애플리케이션 로그에서 실제로 인식하는 스킴/호스트를 찍어 확인하세요.
@GetMapping("/debug/request")
public Map<String, String> debug(HttpServletRequest request) {
Map<String, String> m = new LinkedHashMap<>();
m.put("scheme", request.getScheme());
m.put("serverName", request.getServerName());
m.put("serverPort", String.valueOf(request.getServerPort()));
m.put("requestURL", request.getRequestURL().toString());
m.put("forwardedProto", request.getHeader("X-Forwarded-Proto"));
m.put("forwardedHost", request.getHeader("X-Forwarded-Host"));
m.put("forwardedPort", request.getHeader("X-Forwarded-Port"));
return m;
}
이 endpoint는 운영에 그대로 두지 말고, 점검 후 제거하거나 접근을 제한하세요.
원인 3: redirect URI에 포트가 붙거나 빠짐
IdP에 등록한 값이 https://example.com/login/oauth2/code/google 인데, 애플리케이션이 https://example.com:443/login/oauth2/code/google 또는 https://example.com:8443/... 같은 형태로 만들면 불일치가 납니다.
체크리스트
- 외부에서 실제 접속하는 포트가 443인데 내부는 8080
- Nginx가
X-Forwarded-Port를 8080으로 전달(잘못) - 로드밸런서가 앞에 있고, Nginx는 중간 계층이라
$server_port가 의도와 다름
로드밸런서가 앞단에 있다면, Nginx가 받는 포트가 이미 80일 수 있습니다. 이 경우 $server_port 대신 고정값을 주는 편이 안전할 때가 있습니다.
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
단, 이는 “항상 HTTPS로만 서비스”한다는 전제가 필요합니다.
원인 4: 경로 prefix(/api) 또는 rewrite로 콜백 경로가 변형
Nginx에서 /api prefix를 붙여 백엔드로 프록시하거나, rewrite 를 사용하면 콜백 path가 달라질 수 있습니다.
예를 들어 IdP에는 https://example.com/login/oauth2/code/google 를 등록했는데,
- 프론트는
/api로 라우팅 - Nginx가
/api를 백엔드로 넘김 - 백엔드가 redirect URI를
/api/login/oauth2/code/google로 만들면
IdP 등록값과 달라져 400이 납니다.
해결 방향
- 외부 공개 경로와 내부 백엔드 경로를 동일하게 맞추기
- 또는 OAuth 콜백 endpoint만은 prefix 없이 통과시키기
# OAuth 콜백은 prefix 없이 백엔드로
location /login/oauth2/ {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 나머지는 /api로 프록시
location /api/ {
proxy_pass http://app:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
여기서 proxy_pass 의 trailing slash 유무가 path 결합 방식에 영향을 줍니다. 콜백 경로가 미묘하게 바뀐다면, 먼저 이 부분부터 의심하세요.
원인 5: state/세션 쿠키가 프록시 환경에서 깨짐(겉으로는 redirect_uri처럼 보임)
일부 IdP/라이브러리는 redirect_uri 불일치처럼 보이지만 실제로는 state 검증 실패(세션 쿠키 미전달)로 400이 나는 경우도 있습니다.
대표 케이스:
- HTTPS인데 쿠키가
Secure로 설정되어야 하는데 아니거나(또는 반대) - SameSite 정책 때문에 크로스 사이트 리다이렉트에서 쿠키가 빠짐
- Nginx가
Set-Cookie를 변형하거나 도메인을 바꿔버림
Spring Security의 OAuth2 로그인은 기본적으로 state를 세션에 저장합니다. 콜백에서 세션이 이어지지 않으면 authorization_request_not_found 같은 오류가 납니다.
이 경우는 redirect_uri 자체보다 쿠키/세션 연속성을 먼저 확인하세요.
- 브라우저 개발자도구에서 리다이렉트 전후로 쿠키가 유지되는지
- 콜백 요청에 쿠키가 포함되는지
- 도메인/서브도메인이 바뀌는지
JWT 기반 인증을 함께 쓰는 환경이라면, 토큰 검증 단계에서 다른 오류로 이어질 수도 있습니다. 연관 이슈로는 JWT kid 누락·불일치로 JWKS 검증 실패 해결도 함께 점검해두면 운영 장애 대응에 도움이 됩니다.
실전 진단: “IdP에 전달된 redirect_uri”를 그대로 확인하기
가장 빠른 방법은 IdP로 나가는 authorize 요청 URL을 그대로 복사해 redirect_uri 파라미터를 확인하는 것입니다.
- 브라우저 네트워크 탭에서
authorize요청 확인 - 또는 백엔드에서 authorization redirect를 생성하는 로그를 남김
예시(형태만):
- 기대:
https://example.com/login/oauth2/code/google - 실제:
http://app:8080/login/oauth2/code/google - 실제:
https://example.com:443/login/oauth2/code/google - 실제:
https://example.com/api/login/oauth2/code/google
이 중 무엇이 다른지 확인하면 해결책은 거의 결정됩니다.
Spring Security에서 redirect URI를 명시적으로 고정하는 방법
인프라가 복잡(로드밸런서 + Nginx + 여러 도메인)해서 “자동 재구성”이 계속 흔들린다면, redirect URI를 명시적으로 고정하는 전략도 있습니다.
Spring Security에서 ClientRegistration 의 redirectUri 템플릿을 고정하거나, 환경 변수로 분리할 수 있습니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
redirect-uri: "https://example.com/login/oauth2/code/{registrationId}"
주의:
{registrationId}같은 템플릿은 프레임워크가 해석합니다.- 여기서도 부등호가 아니라 중괄호이므로 MDX 문제는 없지만, 문서/코드 복사 시 오타에 유의하세요.
이 방식은 “항상 단일 도메인”일 때 특히 안정적입니다. 반면 멀티 테넌트/여러 도메인을 지원해야 한다면, 프록시 헤더를 정확히 전달하고 애플리케이션이 이를 신뢰하도록 만드는 편이 확장성이 좋습니다.
Nginx에서 HTTPS 강제 리다이렉트가 만드는 함정
80 포트로 들어온 요청을 443으로 리다이렉트하는 설정 자체는 흔하지만, OAuth 플로우 중간에 불필요한 리다이렉트가 끼면 redirect_uri 와 실제 도달 URL이 어긋나는 경우가 있습니다.
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
이 자체는 보통 안전하지만, 아래 조합이면 문제가 생길 수 있습니다.
- IdP에
http://로 등록(개발용)했는데 운영은 강제 HTTPS - 콜백만 예외 처리해야 하는데 전체 강제 리다이렉트
운영에서는 IdP 등록값도 HTTPS로 통일하는 것이 정석입니다.
운영 체크리스트(재발 방지)
- IdP 콘솔에 등록된 redirect URI를 “외부에서 보이는 최종 URL”로 통일
- Nginx에서
Host,X-Forwarded-Proto,X-Forwarded-Port전달 - Spring Boot에서
server.forward-headers-strategy설정 - 콜백 path에 prefix/rewrite가 개입되는지 확인
- 콜백 시 쿠키가 유지되는지(SameSite, Secure, 도메인) 확인
인프라 계층에서 타임아웃/게이트웨이 문제와 혼동되는 경우도 많습니다. 콜백이 400이 아니라 502/503/timeout으로 보인다면, 네트워크 경로부터 분리 진단이 필요합니다. 이 경우 GCP Cloud Run 503/timeout 원인 7가지 진단법 같은 체크리스트 방식이 도움이 됩니다.
마무리
Nginx 뒤 OAuth 콜백 400의 본질은 “외부에서 보이는 URL”과 “서버가 믿는 URL”의 불일치입니다. 헤더 전달과 애플리케이션의 forwarded 헤더 신뢰 설정만 제대로 맞추면, redirect URI mismatch는 대부분 즉시 해결됩니다.
해결 후에는 브라우저 네트워크 탭에서 authorize 요청의 redirect_uri 값이 IdP 등록값과 완전히 동일한지(스킴/호스트/포트/path)까지 확인하고, 콜백에서 세션 쿠키가 유지되는지도 함께 점검하면 재발 가능성을 크게 낮출 수 있습니다.