Published on

Nginx OAuth 콜백 400 해결 - proxy_set_header·HTTPS

Authors

서드파티 OAuth 로그인은 로컬에서는 잘 되는데, Nginx 뒤에 올리면 콜백에서 갑자기 400 Bad Request가 터지는 경우가 많습니다. 특히 Google, GitHub, Kakao, Naver 같은 공급자들은 redirect_uri 검증이 엄격하고, 애플리케이션 프레임워크는 X-Forwarded-* 헤더와 실제 스킴(HTTP/HTTPS) 불일치에 민감합니다.

이 글은 “Nginx에서 OAuth 콜백이 400으로 실패한다”를 증상 중심으로 재현하고, 어디서 400이 발생하는지(공급자 vs Nginx vs 앱) 를 분리한 뒤, proxy_set_header와 HTTPS(종단/재암호화) 관점에서 실무에서 가장 자주 먹히는 해결책을 정리합니다.

참고: 인프라/배포 문제를 같이 다루는 글로는 Docker BuildKit 캐시가 안 먹을 때 진단·해결도 함께 보면, 재배포 반복을 줄이는 데 도움이 됩니다.

1) 400의 주체부터 분리하기

OAuth 콜백 400은 크게 3군데에서 발생합니다.

  1. OAuth 공급자(Authorization Server)redirect_uri 불일치로 400을 주는 경우
  2. Nginx 가 요청 자체를 “이상한 요청”으로 보고 400을 주는 경우(Host 헤더, 요청 라인, 큰 헤더, 특수문자 등)
  3. 애플리케이션(Backend/Next.js/Spring 등) 이 콜백 파라미터를 검증하다 400을 주는 경우(state, nonce, code verifier 등)

가장 먼저 해야 할 것은 “누가 400을 응답했는지”를 확인하는 것입니다.

빠른 체크: 브라우저 네트워크 탭/서버 로그

  • 브라우저 개발자도구에서 콜백 URL 요청을 클릭해 Response Headersserver 값을 봅니다.
    • server: nginx면 Nginx에서 400을 냈을 가능성이 큽니다.
    • 앱 서버(예: uvicorn, gunicorn, Spring)가 보이면 앱에서 400을 냈을 가능성이 큽니다.
  • Nginx access log에 콜백 요청이 찍히는지 확인합니다.
    • 안 찍히면 공급자에서 콜백 호출 자체가 실패했거나, DNS/방화벽/인증서 문제일 수 있습니다.

Nginx가 400을 내는 전형적 패턴

  • 400 Bad Request 즉시 응답, 앱 로그에는 아무것도 없음
  • Nginx error log에 client sent invalid host header 같은 메시지
  • 리다이렉트 후 콜백 URL이 이상한 형태로 구성(스킴/호스트/포트가 섞임)

2) 가장 흔한 원인: HTTPS인데 앱은 HTTP로 인식

운영에서는 보통 TLS를 Nginx에서 종료합니다.

  • 사용자 https://example.com 접속
  • Nginx가 TLS 종료 후, 업스트림 앱으로는 http://127.0.0.1:3000 같은 형태로 프록시

이때 앱은 “내가 HTTP로 요청을 받았다”고 착각할 수 있습니다. 그러면 앱이 생성하는 콜백/리다이렉트 URL이 http://example.com/...처럼 바뀌고, OAuth 공급자가 등록된 https://example.com/...와 다르다고 판단해 400을 반환합니다.

또는 앱이 state 쿠키를 Secure로 발급해야 하는데 스킴을 HTTP로 인식해 Secure를 붙이지 못하거나, 반대로 SameSite=NoneSecure가 없어 쿠키가 누락되어 앱이 콜백에서 state 검증 실패로 400을 내기도 합니다.

핵심은 앱이 원래 클라이언트의 스킴과 호스트를 정확히 알도록 X-Forwarded-Proto, X-Forwarded-Host 등을 전달하는 것입니다.

3) 정석 Nginx 설정: proxy_set_header 세트

아래는 OAuth 콜백을 포함한 대부분의 웹앱에 안전한 기본값입니다.

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

  # 인증서 설정은 생략

  location / {
    proxy_pass http://127.0.0.1:3000;

    # 업스트림이 원래 요청의 Host를 알게 함
    proxy_set_header Host $host;

    # 클라이언트 IP/체인 전달
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # HTTPS로 들어왔음을 전달 (가장 중요)
    proxy_set_header X-Forwarded-Proto $scheme;

    # 원래 호스트/포트도 전달 (프레임워크에 따라 필요)
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port $server_port;

    # 리다이렉트 Location 헤더를 자동으로 바꾸지 않게
    proxy_redirect off;

    # 웹소켓/업그레이드가 있으면 추가
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }
}

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

여기서 OAuth 콜백 400과 직접적으로 맞닿는 것은 아래 2개입니다.

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

Host가 중요한가

많은 프레임워크가 “내가 어떤 도메인으로 호출되었는지”를 Host로 판단합니다. proxy_pass 대상이 IP인 경우, 잘못 설정되면 업스트림이 127.0.0.1 같은 호스트로 인식해 redirect_uri를 엉뚱하게 만들 수 있습니다.

X-Forwarded-Proto가 중요한가

TLS 종료 후 업스트림은 HTTP로 받기 때문에, 이 헤더가 없으면 스킴을 HTTP로 판단합니다. OAuth에서 스킴 불일치는 치명적입니다.

4) “등록된 redirect_uri와 실제 redirect_uri”를 반드시 비교하기

OAuth 공급자 콘솔에 등록한 콜백 URL(redirect URI)과, 실제 런타임에서 생성되는 URL이 1글자라도 다르면 실패합니다.

다음 항목을 특히 자주 틀립니다.

  • https vs http
  • 도메인 example.com vs www.example.com
  • 포트 :443 표기 유무
  • 경로 트레일링 슬래시 /callback vs /callback/
  • URL 인코딩 차이(특히 쿼리 포함 시)

실전 팁: 콜백 직전 authorize URL을 복사해서 확인

브라우저 주소창에 잠깐 나타나는 authorize URL(또는 네트워크 탭의 request URL)에서 redirect_uri= 값을 확인하세요.

  • 운영에서 redirect_uri=http://...로 찍히면 거의 확실히 X-Forwarded-Proto 문제입니다.
  • redirect_uri의 호스트가 내부 호스트로 찍히면 Host 전달 문제이거나 앱의 base URL 설정 문제입니다.

5) 앱 프레임워크 측 “프록시 신뢰 설정”도 필요하다

Nginx가 헤더를 줘도, 앱이 그 헤더를 신뢰하지 않으면 스킴을 복원하지 못합니다.

Express / Next.js 커스텀 서버 계열

Express를 쓴다면 프록시 신뢰를 켜야 req.protocolX-Forwarded-Proto를 반영합니다.

import express from 'express';

const app = express();
app.set('trust proxy', 1);

app.get('/health', (req, res) => {
  res.json({
    protocol: req.protocol,
    host: req.get('host'),
    xfp: req.get('x-forwarded-proto'),
  });
});

Next.js 자체도 배포 형태에 따라 원래 스킴을 환경변수 기반으로 구성해야 하는 경우가 있습니다. 특히 인증 라이브러리(예: NextAuth 계열)는 NEXTAUTH_URL 같은 base URL 설정이 틀리면 콜백 검증에서 400을 내기도 합니다.

Spring Boot / Spring Security

Spring은 버전/설정에 따라 포워딩 헤더 처리 방식이 다릅니다. 일반적으로는 “Forwarded 헤더 전략”을 명시하거나, 프록시 환경에서 헤더를 읽게 해야 합니다.

예시(환경에 맞게 조정):

server:
  forward-headers-strategy: framework

그리고 리버스 프록시 뒤에서 리다이렉트 URL을 만들 때 스킴이 잘 복원되는지 확인해야 합니다.

성능/지연 이슈까지 엮여 있다면, 서버 스레드/락 진단 관점에서 Spring Boot 3·Java 21 가상스레드 데드락/지연 진단도 같이 참고하면 좋습니다.

6) Nginx 자체가 400을 내는 케이스들

OAuth 콜백은 쿼리스트링이 길어질 수 있고, 공급자에 따라 쿠키/헤더가 커질 수도 있습니다. 다음은 Nginx가 “요청이 비정상”이라며 400을 내는 대표적인 이유입니다.

6-1) 큰 쿠키/헤더로 인한 버퍼 문제

증상:

  • 특정 사용자(쿠키 많은 사용자)만 400
  • error log에 header 관련 메시지

해결(필요한 만큼만, 과도 증설은 지양):

server {
  # ...
  large_client_header_buffers 4 16k;
}

6-2) Host 헤더 검증 실패

server_name과 요청 Host가 맞지 않거나, 예상치 못한 Host가 들어오면 400이 날 수 있습니다.

  • DNS가 www로도 들어오는데 server_name에 누락
  • 로드밸런서 헬스체크가 IP로 들어옴

해결:

server {
  listen 443 ssl;
  server_name example.com www.example.com;
  # ...
}

6-3) HTTP로 들어온 요청을 HTTPS로 강제 리다이렉트하면서 꼬임

80 포트에서 443으로 넘기는 설정이 애매하면, OAuth 공급자 콜백이 HTTP로 호출되었다가 리다이렉트되며 redirect_uri 검증과 충돌할 수 있습니다.

권장 패턴:

server {
  listen 80;
  server_name example.com www.example.com;
  return 301 https://$host$request_uri;
}

이때도 본문에 return 301 https://...처럼 https://는 괜찮지만, 다른 곳에서 부등호 기호를 일반 텍스트로 쓰지 않도록 주의해야 합니다.

7) 체크리스트: 10분 안에 원인 좁히기

A. 공급자 redirect_uri 불일치 의심

  • authorize 요청의 redirect_uri를 확인
  • 등록된 콜백 URL과 스킴/호스트/포트/슬래시까지 완전 일치시키기
  • Nginx에 proxy_set_header Host $host;proxy_set_header X-Forwarded-Proto $scheme; 적용
  • 앱이 프록시 헤더를 신뢰하도록 설정(예: Express trust proxy)

B. 앱에서 state/nonce 검증 실패 의심

  • 콜백 요청이 앱까지 도달하는지(앱 access log) 확인
  • 쿠키가 콜백 요청에 포함되는지 확인
  • HTTPS에서 SameSite=None 쿠키는 Secure가 필수인 점 확인
  • 앱이 스킴을 HTTP로 인식하면 Secure 쿠키가 누락될 수 있으니 X-Forwarded-Proto부터 해결

C. Nginx가 400을 내는지 의심

  • Nginx error log 확인
  • large_client_header_buffers 조정 검토
  • server_name/Host 매칭 확인

8) 재현용 디버그 엔드포인트로 헤더를 눈으로 확인하기

문제가 애매하면 “업스트림이 실제로 어떤 헤더를 받는지”를 먼저 눈으로 확인하는 게 가장 빠릅니다.

Node 예시:

import http from 'http';

const server = http.createServer((req, res) => {
  if (req.url?.startsWith('/debug/headers')) {
    res.setHeader('content-type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({
      url: req.url,
      headers: req.headers,
    }, null, 2));
    return;
  }
  res.statusCode = 200;
  res.end('ok');
});

server.listen(3000);
  • 브라우저에서 /debug/headers를 호출해 x-forwarded-proto, host가 기대값인지 확인합니다.
  • 기대값은 보통 x-forwarded-proto: https, host: example.com 입니다.

9) 결론: OAuth 콜백 400의 80퍼센트는 “스킴/호스트 불일치”다

운영에서 OAuth 콜백 400을 만나면, 먼저 아래를 고정하세요.

  • Nginx에서 proxy_set_header Host $host;
  • Nginx에서 proxy_set_header X-Forwarded-Proto $scheme;
  • 앱에서 프록시 헤더를 신뢰하도록 설정
  • 공급자 콘솔의 redirect URI를 실제 값과 1글자까지 일치

이 4가지만 정리해도 대부분의 400은 사라집니다. 그 다음 단계로 쿠키(SameSite/Secure), 헤더 버퍼, server_name/리다이렉트 정책을 점검하면 “로컬에서는 되는데 운영에서만 실패”하는 OAuth 문제를 체계적으로 끝낼 수 있습니다.