- Published on
Cloudflare 뒤 OAuth 콜백 302 무한루프 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 OAuth(구글/깃허브/애저AD 등)를 붙인 서비스가 Cloudflare 뒤로 들어가면, 특정 조건에서 콜백 URL이 계속 302로 튕기며 로그인 화면으로 되돌아가는 무한루프가 발생합니다. 로컬/직접 접속에서는 정상인데, Cloudflare 프록시(오렌지 구름)만 켜면 터지는 패턴이 흔합니다.
이 글은 “대체 왜 302가 반복되는가?”를 프로토콜(HTTP/HTTPS) 인지, 호스트/포트 인지, 쿠키 속성, 세션 저장, 리다이렉트 URI 관점에서 분해해서, 재현→진단→수정까지 한 번에 끝내는 체크리스트로 정리합니다.
증상: 콜백은 오는데 세션이 안 붙는다
대표적인 증상은 다음과 같습니다.
/oauth/callback혹은/auth/callback로 요청이 들어오지만 응답이 302- 302 Location이 다시
/login혹은 OAuth authorize endpoint로 이동 - 브라우저 네트워크 탭에서 302가 반복
- 서버 로그에는 “state mismatch”, “invalid state”, “CSRF detected”, “session not found” 같은 메시지
- Cloudflare를 우회(직접 오리진)하면 정상
이 상황의 본질은 보통 하나입니다.
> OAuth 플로우에서 저장해둔 state/nonce/세션 쿠키가 콜백 요청에 실려오지 않거나, 앱이 ‘원래 URL’을 잘못 계산해서 다시 authorize로 보내는 것
원인 1) 오리진은 HTTP로 보는데 외부는 HTTPS (X-Forwarded-Proto 미반영)
Cloudflare는 사용자와는 HTTPS로 통신해도, 오리진으로는 설정에 따라 HTTP로 붙을 수 있습니다(또는 TLS 종료 지점이 Cloudflare). 이때 애플리케이션이 다음을 잘못 판단합니다.
- “현재 요청은 http다” → 리다이렉트 URI를 http로 생성
- OAuth provider에는 https 콜백만 등록되어 있음 → 불일치
- 혹은 프레임워크가
Secure쿠키를 http로 판단해 설정하지 않음 → 세션 쿠키 미설정
진단
오리진 앱에서 아래 헤더를 로그로 찍어보세요.
X-Forwarded-ProtoX-Forwarded-HostCF-Visitor(Cloudflare가 JSON 형태로 scheme 정보를 주기도 함)
Cloudflare를 통과한 요청에서 X-Forwarded-Proto: https가 오지 않거나, 앱이 이를 신뢰하지 않으면 루프가 납니다.
해결 (공통)
- Cloudflare SSL/TLS 모드는 가능하면 Full (strict)
- 오리진(리버스 프록시/앱 서버)에서 Forwarded 헤더를 신뢰하도록 설정
- 앱이 생성하는 외부 URL 기준(스킴/호스트)을 “프록시 기준”으로 맞추기
아래는 프레임워크별 대표 설정입니다.
해결 1-A) Node.js(Express)에서 trust proxy 설정
Express는 프록시 뒤에서 req.protocol, req.secure를 제대로 계산하려면 trust proxy가 필요합니다.
import express from "express";
const app = express();
// Cloudflare/로드밸런서 뒤라면 필수
app.set("trust proxy", true);
app.get("/debug", (req, res) => {
res.json({
protocol: req.protocol,
secure: req.secure,
host: req.get("host"),
xfp: req.get("x-forwarded-proto"),
xfh: req.get("x-forwarded-host"),
});
});
trust proxy가 없으면 req.protocol이 http로 고정되어 콜백 URL/리다이렉트 URL 생성이 꼬일 수 있습니다.
해결 1-B) Spring Boot에서 Forwarded 헤더 처리
Spring Boot 2.6+에서는 프록시 헤더 처리 전략을 명시하는 것이 안전합니다.
server:
forward-headers-strategy: framework
또는 톰캣/프록시 구성에 따라 native가 필요한 경우도 있습니다. 핵심은 외부에서 https로 들어온 요청을 앱이 https로 인지하게 만드는 것입니다.
해결 1-C) Django에서 SECURE_PROXY_SSL_HEADER
# settings.py
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
원인 2) 쿠키 SameSite/Secure 설정 때문에 state 세션이 유실
OAuth는 대개 다음 중 하나에 의존합니다.
- 서버 세션 쿠키(예:
sessionid) - 임시 쿠키에 저장한
state/nonce - PKCE verifier를 세션에 저장
그런데 Cloudflare 뒤에서 도메인/스킴이 바뀌거나, OAuth provider → 내 서비스로 돌아오는 과정이 cross-site로 인식되면 쿠키가 빠질 수 있습니다.
특히 2020년 이후 브라우저는 기본 SameSite 정책이 강화되어,
SameSite=Lax는 “일부” 리다이렉트/POST 콜백에서 쿠키가 제외될 수 있고SameSite=None을 쓰려면 반드시Secure가 필요합니다.
흔한 실패 패턴
- 콜백이
POST로 들어오는 provider(또는 프레임워크 설정)인데 쿠키가 안 옴 SameSite=None인데Secure가 빠져서 브라우저가 쿠키를 저장 자체를 거부- http로 인지되어
Secure쿠키가 설정되지 않음(원인 1과 결합)
해결: OAuth 관련 쿠키는 SameSite=None; Secure
Express + express-session 예시
import session from "express-session";
app.set("trust proxy", true);
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // HTTPS 필수
sameSite: "none", // OAuth 리다이렉트 호환
},
})
);
주의: secure: true는 오리진이 https로 인지되어야 정상 동작합니다(그래서 trust proxy가 중요).
Spring Security(개념)
Spring Security에서 세션/쿠키 정책을 건드리는 경우가 많습니다. 무한루프라면 먼저 세션이 유지되는지부터 확인하세요. 콜백 요청에 JSESSIONID가 붙는지, 응답 Set-Cookie가 브라우저에서 차단되지 않는지 확인이 핵심입니다.
추가로 JWT 기반 리소스 서버에서 JWKS 설정이 꼬이면 로그인 후 토큰 검증에서 다시 튕길 수 있는데, 그 경우는 302 루프와 증상이 섞여 보일 수 있습니다. 토큰 검증 문제까지 의심된다면 JWT 검증에서 jwks_uri 404·kid 불일치 해결도 함께 점검하세요.
원인 3) Redirect URI/Host 불일치 (www 유무, 포트, trailing slash)
Cloudflare를 붙이면서 다음 변경이 자주 일어납니다.
example.com↔www.example.com강제 리다이렉트- 오리진은
:8080인데 외부는 443 /oauth/callbackvs/oauth/callback/슬래시 차이
OAuth provider는 등록된 redirect URI와 완전 일치를 요구하는 경우가 많고, 프레임워크는 X-Forwarded-Host를 신뢰하지 않으면 내부 호스트로 redirect URI를 만들어버립니다.
진단
- 브라우저 네트워크에서 authorize 요청의
redirect_uri파라미터를 확인 - provider 콘솔에 등록된 값과 바이트 단위로 일치하는지 확인
- Cloudflare Rules(리다이렉트 규칙)에서 www 강제/HTTP→HTTPS 강제가 콜백 경로에 적용되는지 확인
해결
- 애플리케이션의 “외부 베이스 URL”을 명시적으로 고정(예:
PUBLIC_URL=https://example.com) - Cloudflare Redirect Rules에서 콜백 경로는 예외 처리
예: Nginx 뒤 앱에서 외부 URL을 강제할 때(개념)
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
원인 4) Cloudflare 캐시/자동 최적화가 콜백 엔드포인트를 건드림
OAuth 콜백은 절대 캐시되면 안 됩니다. 하지만 Cloudflare 설정/규칙이 잘못되면 HTML/302 응답이 캐시되어 이상한 루프가 생길 수 있습니다.
체크 포인트
- Cache Rule에 “Cache Everything” 같은 규칙이 있는지
- 콜백 경로(
/oauth/*,/auth/*)가 캐시 예외인지 - Transform Rules로 Location 헤더나 쿠키를 변경하지 않는지
권장 설정
/oauth/*,/auth/*,/login*경로는 Cache Disabled- 응답에
Cache-Control: no-store를 명시(가능하면)
Express에서 콜백에 no-store 넣기 예시:
app.get("/oauth/callback", (req, res, next) => {
res.set("Cache-Control", "no-store");
next();
});
원인 5) Web/App 방화벽(WAF) 또는 Bot Fight가 state 파라미터를 차단
Cloudflare WAF 규칙이나 Bot 관리가 쿼리스트링을 공격으로 오탐할 수 있습니다.
state값이 길고 랜덤 → 의심code파라미터 포함 → 룰에 걸림
진단
- Cloudflare Security Events에서 해당 요청이 Challenge/Block 되었는지 확인
- 브라우저에서 “잠깐 캡차 후 다시 로그인” 같은 이상 행동이 있는지 확인
해결
- 콜백 경로에 대해 WAF 예외(또는 Managed Challenge 완화)
- Bot Fight Mode/JS Challenge를 콜백 경로에서는 끄기
10분 진단 체크리스트 (가장 빨리 잡는 순서)
아래 순서대로 보면 대부분 10분 내 원인이 좁혀집니다.
- 브라우저 DevTools → Network
- 302 Location 체인이 어디서 시작/반복되는지 확인
- 콜백 요청에 쿠키가 포함되는지 확인
- 서버 로그에 헤더 덤프
X-Forwarded-Proto,X-Forwarded-Host,Host
- 세션 쿠키 Set-Cookie 확인
SameSite=None; Secure가 맞는지- 브라우저 콘솔/애플리케이션 탭에서 쿠키가 “차단됨” 표시가 있는지
- OAuth provider redirect URI 일치 확인
- www/슬래시/포트까지
- Cloudflare 캐시/WAF 이벤트 확인
인프라 쪽에서 5xx(502/503)가 섞여 보이면, 루프와 별개로 오리진 장애가 동반된 것일 수 있습니다. EKS/쿠버네티스 환경이라면 EKS에서 503 Service Unavailable 원인 10분 진단처럼 “요청은 도착하는데 백엔드가 불안정해서 인증 플로우가 깨지는” 케이스도 같이 배제하는 게 좋습니다.
실전 예시: “Cloudflare 켜면 무한루프”의 가장 흔한 조합
현장에서 가장 자주 본 조합은 이렇습니다.
- Cloudflare 뒤 오리진은 HTTP
- Express/Spring/Django가
X-Forwarded-Proto를 신뢰하지 않음 - 세션 쿠키가
Secure로 설정되지 않거나,SameSite=Lax라 콜백에서 빠짐 - 결과적으로 state 검증 실패 → 다시 /login으로 302 → 무한루프
이 경우 정답은 보통 2줄입니다.
- 앱: 프록시 신뢰 설정(Express
trust proxy, Springforward-headers-strategy, DjangoSECURE_PROXY_SSL_HEADER) - 쿠키:
SameSite=None; Secure
마무리: “302 루프”는 인증 문제가 아니라 ‘경계면’ 문제다
OAuth 콜백 302 무한루프는 대개 코드 로직 자체가 아니라 Cloudflare(엣지) ↔ 오리진(앱) 경계에서 스킴/호스트/쿠키 정책이 어긋나서 생깁니다.
- 앱이 외부 스킴(https)을 정확히 인지하는가?
- 쿠키가 cross-site 리다이렉트에서 살아남는가?
- redirect URI가 정확히 일치하는가?
- Cloudflare가 콜백을 캐시/차단하지 않는가?
위 4가지만 정리하면, 대부분의 302 무한루프는 재발 없이 정리됩니다.