- Published on
Nginx OAuth 콜백 400 해결 - proxy_set_header·HTTPS
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 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군데에서 발생합니다.
- OAuth 공급자(Authorization Server) 가
redirect_uri불일치로 400을 주는 경우 - Nginx 가 요청 자체를 “이상한 요청”으로 보고 400을 주는 경우(Host 헤더, 요청 라인, 큰 헤더, 특수문자 등)
- 애플리케이션(Backend/Next.js/Spring 등) 이 콜백 파라미터를 검증하다 400을 주는 경우(state, nonce, code verifier 등)
가장 먼저 해야 할 것은 “누가 400을 응답했는지”를 확인하는 것입니다.
빠른 체크: 브라우저 네트워크 탭/서버 로그
- 브라우저 개발자도구에서 콜백 URL 요청을 클릭해 Response Headers의
server값을 봅니다.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=None에 Secure가 없어 쿠키가 누락되어 앱이 콜백에서 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글자라도 다르면 실패합니다.
다음 항목을 특히 자주 틀립니다.
httpsvshttp- 도메인
example.comvswww.example.com - 포트
:443표기 유무 - 경로 트레일링 슬래시
/callbackvs/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.protocol이 X-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 문제를 체계적으로 끝낼 수 있습니다.