Published on

OAuth redirect_uri 불일치 8원인과 즉시 해결

Authors

OAuth 로그인 연동을 붙일 때 redirect_uri mismatch 또는 invalid_redirect_uri는 거의 필연적으로 한 번은 만나게 됩니다. 문제는 에러 메시지가 대체로 불친절하다는 점입니다. 실제로는 redirect_uri가 "조금이라도" 다르면(스킴, 호스트, 포트, 경로, 슬래시, 인코딩, 프록시 헤더 등) 공급자(Authorization Server)가 즉시 거부합니다.

이 글은 현장에서 가장 많이 터지는 8가지 원인을 기준으로, "지금 당장" 어디를 고쳐야 하는지에 초점을 맞춘 해결 가이드입니다. 마지막에는 재발 방지를 위한 운영 체크리스트도 제공합니다.

관련해서 프록시 환경에서 특히 자주 발생하는 케이스는 아래 글도 함께 참고하면 빠르게 정리됩니다.

먼저 확인: 에러가 의미하는 것

OAuth 2.0 Authorization Code Flow에서 클라이언트는 보통 다음과 같이 인가 엔드포인트로 이동합니다.

  • 사용자가 보는 브라우저 URL(인가 요청)
  • 서버가 비교하는 등록된 redirect_uri 목록

공급자는 "등록된 값"과 "요청으로 들어온 값"이 정확히 일치하는지 비교합니다. 여기서 redirect_uri는 보안상 민감한 값이라, 많은 공급자가 부분 일치나 와일드카드를 허용하지 않거나 매우 제한적으로만 허용합니다.

빠른 진단 1분 체크

  1. 브라우저 주소창(또는 네트워크 탭)에서 인가 요청 URL을 복사
  2. 그 URL의 redirect_uri 파라미터를 디코딩
  3. 공급자 콘솔에 등록된 Redirect URI와 글자 단위로 비교

이때 비교 대상은 "디코딩된 최종 문자열"입니다.

아래는 Node.js에서 인가 URL을 만들 때 흔히 쓰는 예시입니다.

const params = new URLSearchParams({
  response_type: 'code',
  client_id: process.env.OAUTH_CLIENT_ID,
  redirect_uri: 'https://app.example.com/oauth/callback',
  scope: 'openid profile email',
  state: crypto.randomUUID(),
});

const authorizeUrl = `https://idp.example.com/oauth2/authorize?${params.toString()}`;
console.log(authorizeUrl);

원인 1) 스킴 불일치: http vs https

가장 흔합니다. 로컬에서는 http://localhost:3000/callback을 쓰다가, 운영에서는 https://app.example.com/callback로 바뀝니다. 또는 프록시/로드밸런서 뒤에서 앱이 자신을 http로 인식해 redirect_urihttp로 생성하는 경우도 많습니다.

즉시 해결

  • 공급자 콘솔에 실제 운영 스킴으로 등록
  • 앱에서 redirect_uri를 "요청 기반 자동 조립"이 아니라 "명시적 환경변수"로 고정
OAUTH_REDIRECT_URI=https://app.example.com/oauth/callback
const redirectUri = process.env.OAUTH_REDIRECT_URI;

원인 2) 호스트 불일치: www/서브도메인/도메인 혼용

example.comwww.example.com은 다른 호스트입니다. app.example.comexample.com도 마찬가지입니다. 프론트에서만 www로 리다이렉트되거나, CDN 설정으로 canonical host가 바뀌면 바로 불일치가 납니다.

즉시 해결

  • 사용자 진입 호스트를 하나로 강제(301 리다이렉트)
  • OAuth 등록 Redirect URI도 그 호스트 하나로 통일
  • 프론트/백엔드 모두 동일한 OAUTH_REDIRECT_URI를 참조

원인 3) 포트 불일치: :80, :443, 개발 포트

https://example.com:443/callbackhttps://example.com/callback은 문자열로는 다릅니다(공급자 구현에 따라 다르게 취급될 수 있음). 로컬 개발에서는 http://localhost:3000처럼 포트가 필수라서 더 자주 터집니다.

즉시 해결

  • 로컬용 Redirect URI를 별도로 등록
  • 환경별로 redirect_uri를 분리 관리
# local
OAUTH_REDIRECT_URI=http://localhost:3000/oauth/callback

# prod
OAUTH_REDIRECT_URI=https://app.example.com/oauth/callback

원인 4) 경로(path) 불일치: 한 글자만 달라도 실패

/oauth/callback/oauth/callback/은 다릅니다. /auth/callback로 바뀌었는데 콘솔은 예전 값이면 바로 실패합니다.

즉시 해결

  • 콜백 라우트를 변경했다면 공급자 콘솔도 함께 변경
  • 코드에 하드코딩된 경로가 여러 군데 있는지 grep로 확인
rg "oauth/callback" -n

원인 5) 트레일링 슬래시(/) 불일치

특히 Next.js, Spring, Nginx 리라이트 규칙이 개입하면 트레일링 슬래시가 자동으로 붙거나 제거되기도 합니다.

즉시 해결

  • 등록값과 요청값을 동일하게 맞추기(붙이거나 빼거나)
  • 프레임워크의 trailingSlash 옵션(예: Next.js)과 리버스 프록시 rewrite 규칙을 정합성 있게 구성

원인 6) URL 인코딩/디코딩 이슈: 이중 인코딩, 쿼리 포함

인가 요청에서는 redirect_uri가 쿼리 파라미터로 들어가기 때문에 인코딩이 필수입니다. 그런데 어떤 라이브러리는 이미 인코딩된 값을 또 인코딩해버립니다. 그러면 공급자가 디코딩했을 때 원래 값이 달라져 불일치가 납니다.

예를 들어 https://app.example.com/oauth/callback?from=login 같은 값은 인가 요청 URL에서 %3F, %3D로 인코딩되어야 합니다.

즉시 해결

  • redirect_uri는 "원문 URL"로 관리하고, 인가 URL 생성 시에만 URLSearchParams 등에 맡기기
  • 이미 인코딩된 문자열을 encodeURIComponent로 또 감싸지 않기
// 좋음: 원문을 넣고 URLSearchParams가 인코딩하도록 둠
const redirectUri = 'https://app.example.com/oauth/callback?from=login';
const params = new URLSearchParams({ redirect_uri: redirectUri });

// 나쁨: 이중 인코딩 위험
const bad = encodeURIComponent(redirectUri);
const params2 = new URLSearchParams({ redirect_uri: bad });

원인 7) 프록시/로드밸런서 뒤에서 "외부 URL"을 잘못 추론

앱이 req.protocol이나 Host 헤더를 보고 redirect_uri를 동적으로 만들 때, 프록시 뒤에서는 내부 프로토콜이 http로 보이거나 내부 호스트가 들어오는 일이 흔합니다. 특히 X-Forwarded-Proto, X-Forwarded-Host를 신뢰하지 않으면 외부 기준 URL을 만들 수 없습니다.

즉시 해결

  • 가장 안전한 방식은 redirect_uri를 환경변수로 고정
  • 어쩔 수 없이 동적 구성이라면 프록시 헤더 신뢰 설정을 올바르게 적용

Express 예시:

import express from 'express';

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

app.get('/login', (req, res) => {
  const proto = req.protocol; // trust proxy 설정이 중요
  const host = req.get('host');
  const redirectUri = `${proto}://${host}/oauth/callback`;
  res.send(redirectUri);
});

Nginx 예시(핵심 헤더 전달):

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;

프록시 뒤에서의 대표 해결 패턴은 아래 글에서 더 깊게 다룹니다.

원인 8) 공급자 콘솔 설정 실수: 환경/앱/테넌트 혼동, 여러 Redirect URI 관리 실패

의외로 "코드는 맞는데" 콘솔에서 다른 앱(Client ID)을 보고 있거나, 개발용 테넌트/운영용 테넌트를 혼동하는 경우가 많습니다.

자주 나오는 패턴:

  • 운영에서 개발 Client ID를 사용
  • 모바일/웹 클라이언트가 같은 공급자 프로젝트에 섞여 있고 Redirect URI가 뒤엉킴
  • 콜백 URL을 바꿨는데 콘솔 저장을 안 했거나, 검수/배포 반영이 지연됨

즉시 해결

  • 현재 실행 중인 서비스가 사용하는 client_id를 로그로 출력하고 콘솔에서 정확히 그 클라이언트를 열어 확인
  • 환경별로 OAuth 클라이언트를 분리(개발/스테이징/운영)
console.log('OAUTH_CLIENT_ID=', process.env.OAUTH_CLIENT_ID);
console.log('OAUTH_REDIRECT_URI=', process.env.OAUTH_REDIRECT_URI);

재현 가능한 디버깅 루틴(현장용)

아래 순서로 하면 대부분 5분 내로 원인에 도달합니다.

  1. 브라우저 네트워크 탭에서 인가 요청 URL 확인
  2. redirect_uri 값만 따로 복사해서 디코딩
  3. 공급자 콘솔 등록값과 "완전 일치" 비교
  4. 불일치가 없는데도 실패하면
    • 다른 client_id를 쓰는지 확인
    • 프록시/리라이트로 실제 콜백 경로가 변경되는지 확인
    • 이중 인코딩 여부 확인

디코딩은 Node.js로도 바로 확인 가능합니다.

const encoded = 'https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback%3Ffrom%3Dlogin';
console.log(decodeURIComponent(encoded));

운영에서 덜 깨지게 만드는 설계 팁

1) redirect_uri를 절대 동적 조립하지 않기

가능하면 OAUTH_REDIRECT_URI를 환경변수로 고정하세요. 프록시, 멀티 도메인, CDN, 스킴 전환 같은 변수가 늘어날수록 동적 조립은 사고 확률이 급격히 올라갑니다.

2) 환경별 OAuth 클라이언트 분리

개발/스테이징/운영을 같은 Client ID로 쓰면 콘솔의 Redirect URI 목록이 오염됩니다. 결국 "어느 환경에서든 동작"하게 만들려다 보안 정책에 막히거나, 예상치 못한 리다이렉트가 허용될 수 있습니다.

3) 콜백 엔드포인트에서 실제로 받은 값을 로깅

인가 단계에서 실패하면 콜백까지 오지 않지만, 토큰 교환 단계에서 redirect_uri를 다시 보내는 공급자도 있습니다(또는 라이브러리가 보냄). 이때 서버에서 토큰 요청 페이로드의 redirect_uri를 로그로 남기면 큰 도움이 됩니다.

예: 토큰 요청(형태 예시)

curl -s -X POST https://idp.example.com/oauth2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'code=AUTH_CODE' \
  -d 'redirect_uri=https://app.example.com/oauth/callback'

마무리: 가장 많이 맞는 정답은 "문자열 완전 일치"

redirect_uri 불일치는 결국 "등록된 문자열"과 "요청으로 보낸 문자열"이 완전히 같지 않아서 발생합니다. 스킴/호스트/포트/경로/슬래시/인코딩/프록시/콘솔 설정까지 8가지를 순서대로 점검하면, 대부분은 코드 수정 없이 설정만으로도 즉시 해결됩니다.

프록시나 Nginx가 끼어 있는 구조라면, 외부에서 보이는 URL과 내부 앱이 인식하는 URL을 일치시키는 것이 핵심이므로 아래 글을 함께 보면 재발 방지에 특히 도움이 됩니다.