Published on

Proxy 뒤 Nginx에서 OAuth 리다이렉트 URI 불일치 해결

Authors

서버를 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는 보안상 이유로 사전 등록된 값과 완전히 일치해야 합니다. 즉,

  • 스킴(http vs https)
  • 호스트
  • 포트
  • 경로
  • (일부 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를 만들 수 있습니다.
  • $schemeNginx가 받은 스킴입니다. 만약 Nginx 앞단에서 TLS가 종료되고 Nginx는 HTTP만 받는다면 $scheme은 항상 http가 됩니다. 이 경우는 다음 원인(원인 2)로 넘어가야 합니다.

원인 2: TLS가 앞단에서 종료되어 Nginx는 항상 http로 인식

가장 흔한 케이스입니다.

  • 외부: https://app.example.com
  • 프록시(예: ALB)에서 TLS 종료
  • 내부: http://nginx:80로 전달

이때 Nginx의 $schemehttp이므로 위 설정을 그대로 쓰면 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가지 확인

운영에서 재현이 어려워서 삽질이 길어지는 케이스가 많아, 체크리스트 형태로 정리합니다.

  1. IdP에 등록된 Redirect URI가 정확한가? (스킴/호스트/경로)
  2. 브라우저에서 authorize 요청의 redirect_uri가 무엇으로 나가는가?
  3. 프록시가 X-Forwarded-Proto: https를 넣어주는가?
  4. Nginx가 그 헤더를 upstream으로 전달하는가?
  5. 앱이 forwarded 헤더를 신뢰하도록 설정되어 있는가? (Spring forward-headers-strategy, Express trust proxy)
  6. Nginx가 proxy_redirect로 Location을 재작성하고 있지 않은가?
  7. 멀티 프록시 체인이라면(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는 재발 확률이 크게 줄어듭니다.