Published on

Cloudflare 뒤에서 OAuth 콜백이 http로 떨어질 때

Authors

서드파티 OAuth(구글/깃허브/카카오 등)를 붙여두고 Cloudflare(프록시, Load Balancer, Cloudflare Tunnel) 뒤로 서비스를 옮긴 뒤, 갑자기 콜백이 https://가 아니라 http://로 “떨어지는” 현상을 자주 봅니다. 증상은 대개 아래 중 하나로 나타납니다.

  • IdP 콘솔에는 https://app.example.com/oauth/callback로 등록했는데, 실제 앱이 만든 redirect_urihttp://app.example.com/oauth/callback로 나가서 redirect_uri_mismatch 발생
  • 로그인은 되는데 콜백에서 다시 http로 리다이렉트되어 무한 리다이렉트/쿠키 누락(secure cookie)로 세션이 증발
  • 특정 경로(예: /auth/callback)만 http로 계산되어 프레임워크가 절대 URL 생성에 실패

핵심 원인은 단순합니다. 클라이언트↔Cloudflare 구간은 HTTPS인데, Cloudflare↔원본(origin) 구간이 HTTP이거나, 원본 앱이 “원래 요청이 HTTPS였다”는 정보를 신뢰하지 못해 스킴을 http로 판단하는 겁니다.

이 글에서는 Cloudflare 설정, 프록시 헤더, 그리고 Node/Express·Next.js·Spring Boot 등에서의 실전 수정 포인트를 한 번에 정리합니다.

왜 http로 인식될까: 스킴 결정 로직과 프록시 헤더

웹 앱/프레임워크는 보통 다음 중 하나로 “현재 요청의 스킴(https/http)”을 결정합니다.

  1. 서버가 실제로 받은 연결의 TLS 여부 (origin이 HTTPS로 받으면 https)
  2. 리버스 프록시가 넣어주는 X-Forwarded-Proto: https, 또는 표준인 Forwarded: proto=https
  3. Cloudflare가 넣어주는 CF-Visitor: {"scheme":"https"} 같은 벤더 헤더

Cloudflare를 쓰면 브라우저는 https://로 접속하지만, 원본은 종종 http://로 받습니다(특히 Flexible SSL, Tunnel, 내부 LB). 이때 앱이 (2)를 신뢰하지 않으면 스킴은 http로 결정되고, OAuth 라이브러리가 redirect_urihttp로 만들어버립니다.

1차 점검: 실제로 어떤 헤더가 들어오는지부터 확인

먼저 원본 서버에서 요청 헤더를 덤프해 보세요. Cloudflare 뒤라면 대개 아래가 보입니다.

  • X-Forwarded-Proto: https
  • X-Forwarded-For: <client-ip>, <cf-ip>
  • CF-Visitor: {"scheme":"https"}
  • CF-Connecting-IP: <client-ip>

Node/Express에서 헤더 확인 예시

import express from 'express';

const app = express();

app.get('/debug/headers', (req, res) => {
  res.json({
    protocol: req.protocol,
    secure: req.secure,
    host: req.get('host'),
    xfp: req.get('x-forwarded-proto'),
    forwarded: req.get('forwarded'),
    cfVisitor: req.get('cf-visitor'),
  });
});

app.listen(3000);

여기서 x-forwarded-protohttps인데 req.protocolhttp로 나오면, 프레임워크가 프록시 헤더를 신뢰하지 않는 상태입니다.

Cloudflare 설정에서 가장 흔한 원인: SSL/TLS 모드

Cloudflare 대시보드의 SSL/TLS 모드는 origin까지의 암호화 여부를 좌우합니다.

  • Flexible: 브라우저↔Cloudflare는 HTTPS, Cloudflare↔origin은 HTTP
  • Full: Cloudflare↔origin도 HTTPS(인증서 검증은 느슨)
  • Full (strict): Cloudflare↔origin HTTPS + 유효한 인증서 검증

OAuth 콜백이 http로 떨어지는 사례의 상당수는 Flexible에서 시작합니다. 가능하면 **Full (strict)**로 올리고 origin에도 정상 인증서를 두는 게 정석입니다.

다만, 이미 네트워크 구조상 origin이 HTTP일 수 있습니다(내부망, k8s Ingress, Tunnel). 이 경우에도 해결은 가능합니다. 핵심은 “원래는 HTTPS였다”는 사실을 앱이 알게 하는 것입니다.

해결 전략 A: 앱이 프록시 헤더를 신뢰하도록 설정

Express: app.set('trust proxy', true)

Express는 기본적으로 X-Forwarded-*를 신뢰하지 않습니다. Cloudflare/로드밸런서 뒤라면 아래 설정이 거의 필수입니다.

import express from 'express';

const app = express();

// Cloudflare/프록시 뒤에서 https 판단을 위해 필요
app.set('trust proxy', true);

app.get('/debug/proto', (req, res) => {
  res.json({ protocol: req.protocol, secure: req.secure });
});

app.listen(3000);
  • trust proxy가 켜지면 req.protocolX-Forwarded-Proto를 반영합니다.
  • OAuth 라이브러리(예: Passport, NextAuth, custom)가 절대 URL을 만들 때 이 값이 중요합니다.

보안 팁: 무조건 true 대신, 가능하면 신뢰할 프록시 범위를 좁히세요.

// 예: 첫 번째 홉만 신뢰
app.set('trust proxy', 1);

Spring Boot: Forwarded 헤더 처리 활성화

Spring은 환경에 따라 X-Forwarded-Proto를 자동 반영하지 않을 수 있습니다. Boot 3 기준으로는 다음 중 하나가 필요합니다.

application.yml

server:
  forward-headers-strategy: framework

또는(인프라가 Forwarded 표준 헤더를 쓰는 경우)

server:
  forward-headers-strategy: native

이 설정이 없으면 HttpServletRequest#getScheme()http로 남아 OAuth redirect URL 생성이 틀어질 수 있습니다.

해결 전략 B: OAuth 라이브러리에 “외부 URL(issuer/baseUrl)”을 명시

프레임워크가 스킴을 잘 판단하게 만드는 게 1순위지만, 운영에서는 구성 실수/미들웨어 순서 때문에 다시 깨지는 경우가 있습니다. 이때는 OAuth 구성에 외부에서 보이는 base URL을 명시해 “절대 URL 생성”을 고정하는 방법이 강력합니다.

NextAuth(Next.js)라면 NEXTAUTH_URL

NEXTAUTH_URL=https://app.example.com

이 값이 없거나 잘못되면, 프록시 뒤에서 콜백이 http로 생성될 수 있습니다.

(일반) OAuth 클라이언트에서 redirect_uri를 고정

서버가 redirect_uri를 동적으로 만들지 말고, 환경변수로 고정하는 방식도 자주 씁니다.

OAUTH_REDIRECT_URI=https://app.example.com/oauth/callback
const redirectUri = process.env.OAUTH_REDIRECT_URI;

const authUrl = new URL('https://idp.example.com/oauth/authorize');
authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('response_type', 'code');

장점: 프록시/헤더/프레임워크 변화에도 redirect_uri가 흔들리지 않습니다. 단점: 멀티 도메인/멀티 테넌트에는 추가 설계가 필요합니다.

해결 전략 C: Cloudflare에서 HTTPS 강제(리다이렉트) + Origin 리다이렉트 금지

Cloudflare는 엣지에서 Always Use HTTPS 또는 Redirect Rules로 HTTP→HTTPS를 강제할 수 있습니다. 하지만 여기서 자주 하는 실수가 있습니다.

  • origin 앱도 동시에 HTTP→HTTPS 리다이렉트를 수행
  • 그런데 origin은 “내가 받은 건 HTTP”라서 https로 리다이렉트
  • Cloudflare는 이미 https로 왔는데, origin이 또 리다이렉트하면서 루프/스킴 꼬임 발생

권장 패턴:

  1. 엣지(Cloudflare)에서 HTTPS 강제
  2. origin 앱은 프록시 헤더 신뢰(X-Forwarded-Proto) 후, 필요 시에만 리다이렉트

즉, origin이 무조건적인 스킴 리다이렉트를 하지 않게 조정하는 게 안전합니다.

Cloudflare Tunnel을 쓸 때의 포인트

Cloudflare Tunnel(cloudflared)은 origin과 Cloudflare 간에 터널을 만들고, 로컬 서비스는 보통 HTTP로 붙입니다. 이 구조에서는 원본 앱이 TLS를 직접 보지 못하므로 프록시 헤더 신뢰가 거의 필수입니다.

cloudflared 구성은 대개 이런 형태입니다.

ingress:
  - hostname: app.example.com
    service: http://localhost:3000
  - service: http_status:404

이때도 Cloudflare는 X-Forwarded-Proto: https를 붙여주므로, 앱에서 이를 반영하도록 설정해야 합니다(Express trust proxy, Spring forward-headers-strategy 등).

실전 디버깅 체크리스트(10분 컷)

  1. Cloudflare SSL/TLS 모드 확인: 가능하면 Full (strict)
  2. 원본에서 /debug/headersX-Forwarded-Protohttps인지 확인
  3. 앱이 그 헤더를 반영하는지 확인
    • Express: trust proxy
    • Spring Boot: server.forward-headers-strategy
  4. OAuth 라이브러리가 생성하는 redirect_uri를 실제 로그로 확인
  5. 쿠키 이슈 동반 시(secure cookie): 콜백이 http로 인식되면 Secure 쿠키가 누락될 수 있으니 함께 점검

자주 겪는 함정: “Cloudflare만 믿었는데” 앱 레벨에서 깨지는 이유

  • 미들웨어 순서: Express에서 trust proxy를 너무 늦게 설정하거나, 프록시 앞단에서 헤더를 덮어씀
  • 다중 프록시: LB → Nginx → app처럼 홉이 여러 개인데 trust proxy를 1로 고정해 잘못 판단
  • 리다이렉트 URL 생성 위치: OAuth 핸들러가 request를 참조하지 않고 별도 유틸에서 URL을 만들면서 스킴이 기본값(http)으로 고정

이런 경우에는 구성으로만 해결하려다 시간이 새기 쉽습니다. 로그로 redirect_uri 문자열을 직접 찍어 어디에서 http가 결정되는지 추적하는 게 가장 빠릅니다.

보안 관점: 프록시 헤더 신뢰는 “누구를 믿을지”의 문제

X-Forwarded-Proto는 클라이언트가 위조할 수도 있습니다. 그래서 앱은 “인터넷에서 직접 들어오는 요청”에 대해 이 헤더를 무조건 신뢰하면 안 됩니다.

하지만 Cloudflare 뒤에서만 서비스되고, origin이 외부에 직접 노출되지 않도록 방화벽/보안그룹으로 막혀 있다면(Cloudflare IP만 허용, Tunnel 사용 등) trust proxy를 켜는 것이 합리적입니다.

관련해서 함께 보면 좋은 글

OAuth 콜백 문제를 해결하고 나면, 다음 단계로는 토큰 검증/키 회전(JWKS)에서 401을 줄이는 작업이 자주 따라옵니다. 운영에서 특히 효과가 컸던 패턴은 아래 글에 정리해 두었습니다.

또, 프록시/클라우드 환경에서 “정상처럼 보이는데 간헐적으로만 실패”하는 네트워크 류 이슈는 원인 분리가 중요합니다. 비슷한 접근 방식으로 정리한 글도 참고가 됩니다.

결론: 정답은 ‘https를 끝까지 유지’ 또는 ‘https였음을 정확히 전파’

Cloudflare 뒤에서 OAuth 콜백이 http로 떨어지는 문제는 대부분 아래 둘 중 하나로 깔끔하게 끝납니다.

  • 가능하면 Cloudflare SSL/TLS를 **Full (strict)**로 올려 origin까지 HTTPS를 유지
  • 구조상 origin이 HTTP라면, X-Forwarded-Proto(또는 Forwarded)를 앱이 신뢰하도록 설정하고, 필요 시 OAuth의 base URL/redirect_uri를 환경변수로 고정

이 조합으로 redirect_uri_mismatch, 무한 리다이렉트, secure cookie 누락 같은 2차 증상까지 함께 정리할 수 있습니다.