Published on

OAuth 콜백 400 redirect_uri_mismatch 즉시 해결

Authors

OAuth 로그인 연동을 붙이다 보면, 인증 화면까지는 잘 갔다가 콜백에서 갑자기 400 과 함께 redirect_uri_mismatch 가 터지는 경우가 가장 흔합니다. 이 에러는 “내가 보낸 redirect_uri 가 Provider에 등록된 값과 정확히 일치하지 않는다”는 뜻인데, 문제는 어디가 어떻게 다른지가 로그 한 줄로는 잘 안 보인다는 점입니다.

이 글은 콜백 400/redirect_uri_mismatch즉시 해결하기 위한 실무형 가이드입니다. Provider 콘솔 설정부터, 프록시/로드밸런서 뒤에서 스킴이 바뀌는 문제, Next.js 및 Spring Security에서 실제로 어떤 값이 redirect_uri 로 나가는지까지 빠르게 점검합니다.

관련해서 stateredirect_uri 로 401 루프가 생기는 케이스도 자주 같이 나오니, 필요하면 아래 글도 함께 보시면 좋습니다.


1) 에러의 본질: “문자열 완전 일치” 문제

대부분의 OAuth Provider는 redirect_uri문자열 완전 일치(exact match) 로 비교합니다. 즉, 아래 중 하나라도 다르면 mismatch 입니다.

  • 스킴: http vs https
  • 호스트: example.com vs www.example.com vs api.example.com
  • 포트: :80, :443, :3000
  • 경로: /oauth/callback vs /oauth/callback/ (슬래시 유무)
  • 쿼리: 등록은 쿼리 없음인데 요청에는 ?foo=bar 가 붙음(Provider 정책에 따라 허용/불허)
  • URL 인코딩 차이(특히 쿼리가 붙는 경우)

중요한 점은 “내 앱이 생각하는 콜백 URL”이 아니라, 실제로 Provider에 전달된 redirect_uri 를 확인해야 한다는 것입니다.


2) 30초 진단: 실제 redirect_uri 를 눈으로 확인하기

2-1) 브라우저 주소창에서 authorization 요청을 복사

로그인 버튼을 눌러 Provider로 이동한 뒤, 브라우저 주소창의 authorize URL에서 redirect_uri 파라미터를 찾아 그대로 복사합니다. 보통 이런 형태입니다.

https://accounts.google.com/o/oauth2/v2/auth?...&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback&...

여기서 redirect_uri 값은 URL 인코딩되어 있으니 디코딩해서 비교합니다.

2-2) 로컬에서 빠르게 디코딩

node -e "console.log(decodeURIComponent(process.argv[1]))" "https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback"

이렇게 얻은 값이 Provider 콘솔에 등록된 Redirect URI 목록 중 하나와 완전히 동일해야 합니다.


3) 가장 흔한 원인 TOP 7과 해결

3-1) http 로 나가는데 콘솔에는 https 만 등록됨

개발/스테이징에서 특히 흔합니다.

  • 앱은 http://localhost:3000/callback 로 요청
  • 콘솔에는 https://dev.example.com/callback 만 등록

해결:

  • 개발 환경용 Redirect URI를 Provider에 추가 등록하거나
  • 개발도 https 로 통일(예: 로컬 TLS, 터널링)합니다.

주의: 일부 Provider는 http 를 localhost에만 허용합니다. 즉, http://dev.example.com 같은 형태는 정책상 등록이 안 될 수 있습니다.

3-2) 프록시/로드밸런서 뒤에서 스킴이 바뀜 (X-Forwarded-Proto 미반영)

애플리케이션은 내부에서 http 로 요청을 받지만, 외부 사용자는 https 로 접근하는 구조에서 자주 발생합니다.

  • 외부: https://app.example.com
  • 내부(서버가 인지): http://app.example.com
  • 결과: redirect_urihttp 로 생성되어 mismatch

해결은 “서버가 원래 스킴이 https 임을 알게” 만드는 것입니다.

Spring Boot / Spring Security에서의 대표 해결

server.forward-headers-strategy=framework

또는 인프라에서 X-Forwarded-Proto: https 헤더를 올바르게 넣고, 애플리케이션이 이를 신뢰하도록 설정합니다.

Kubernetes Ingress, ALB, Nginx 등에서 헤더가 누락되면 계속 재발합니다. 특히 Cloud Run, ECS, EKS 같은 환경에서 이 이슈가 반복되기 쉬우니, 배포 환경을 바꾼 직후 가장 먼저 의심하세요.

3-3) www 유무, 서브도메인 불일치

app.example.comwww.example.com 은 완전히 다른 호스트입니다.

해결:

  • 실제 authorize 요청에 나가는 호스트로 통일
  • 또는 Provider 콘솔에 둘 다 등록(가능한 경우)

3-4) 경로 끝 슬래시 차이

/oauth/callback/oauth/callback/ 은 문자열이 다릅니다.

해결:

  • 라우팅을 한쪽으로 고정
  • Provider 콘솔에도 정확히 같은 경로로 등록

Next.js에서 trailing slash 설정을 바꾸면 콜백 URL이 함께 바뀔 수 있습니다.

3-5) 쿼리를 붙여 보내는 구현

일부 라이브러리/구현에서 redirect_uri 에 임의 쿼리를 붙여 보내는 경우가 있습니다.

예: https://app.example.com/oauth/callback?provider=google

Provider 콘솔에 쿼리까지 포함해서 등록해야 하는 Provider도 있고, 쿼리를 아예 허용하지 않는 Provider도 있습니다.

권장:

  • redirect_uri 는 쿼리 없이 고정
  • 추가 정보는 state 에 넣고 서버에서 검증

3-6) 인코딩 이슈 (특히 쿼리 포함 시)

redirect_uri 자체에 쿼리가 있으면 인코딩/디코딩 단계에서 차이가 생길 수 있습니다.

예: redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback%3Fa%3D1%26b%3D2

해결:

  • 가능하면 쿼리 없는 redirect로 단순화
  • 라이브러리가 자동 인코딩하는데 또 인코딩을 한 번 더 하지 않았는지 확인

3-7) Provider 콘솔에 “Authorized redirect URIs”가 아니라 다른 곳에 등록

Provider마다 UI가 다르고, 다음을 혼동하기 쉽습니다.

  • Authorized JavaScript origins
  • Website URL
  • Callback URL
  • Redirect URI

해결:

  • “OAuth Client” 단위로 “Redirect URI” 항목에 정확히 등록
  • 동일 프로젝트 내 다른 클라이언트에 등록해놓고 착각하는 경우도 많습니다.

4) Next.js에서 redirect_uri 를 고정하는 실전 예시

Next.js에서 Provider로 리다이렉트할 때, 환경에 따라 base URL이 흔들리면 mismatch가 반복됩니다. 가장 안전한 방법은 서버에서 base URL을 명시적으로 구성하는 것입니다.

4-1) 환경변수로 base URL을 고정

// lib/url.ts
export function getPublicBaseUrl() {
  const base = process.env.PUBLIC_BASE_URL
  if (!base) throw new Error('PUBLIC_BASE_URL is required')
  return base.replace(/\/$/, '')
}
// app/api/auth/login/route.ts
import { NextResponse } from 'next/server'
import { getPublicBaseUrl } from '@/lib/url'

export async function GET() {
  const baseUrl = getPublicBaseUrl()
  const redirectUri = `${baseUrl}/api/auth/callback/google`

  const url = new URL('https://accounts.google.com/o/oauth2/v2/auth')
  url.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID || '')
  url.searchParams.set('redirect_uri', redirectUri)
  url.searchParams.set('response_type', 'code')
  url.searchParams.set('scope', 'openid email profile')

  return NextResponse.redirect(url.toString())
}

포인트:

  • PUBLIC_BASE_URLhttps://app.example.com 처럼 정확히 고정
  • trailing slash 제거로 문자열 변형 최소화

4-2) Vercel 프리뷰/스테이징

프리뷰 URL이 매번 바뀌면 Provider 콘솔에 전부 등록할 수 없어 막힙니다.

대안:

  • 프리뷰에서는 OAuth를 비활성화하거나
  • 고정 도메인 스테이징을 따로 두거나
  • Provider가 와일드카드 redirect를 지원하는지 확인(대부분은 보안상 불가)

5) Spring Security OAuth2에서 자주 터지는 지점

Spring Security는 redirect_uri 를 내부적으로 구성할 때 요청 정보(스킴/호스트/포트)를 기반으로 만듭니다. 따라서 프록시 환경에서 X-Forwarded-* 처리가 안 되면 mismatch가 납니다.

5-1) application 설정

server.forward-headers-strategy=framework

5-2) 등록값과 실제 콜백 엔드포인트 일치 확인

Spring Security 기본 콜백 패턴은 보통 다음 형태입니다.

/login/oauth2/code/{registrationId}

여기서 {registrationId} 는 예를 들어 google 입니다. 즉 콜백은

https://app.example.com/login/oauth2/code/google

가 됩니다. Provider 콘솔에 이 값을 정확히 등록해야 합니다.

추가로, redirect-uri 템플릿을 커스터마이징했다면 그 결과가 무엇인지 다시 확인하세요.


6) 운영에서 재발 방지: 체크리스트

배포 전에 아래를 자동 점검하면 운영 장애를 크게 줄일 수 있습니다.

6-1) 체크리스트

  • PUBLIC_BASE_URL 또는 서비스 외부 URL이 환경별로 고정되어 있는가
  • Provider 콘솔에 등록된 Redirect URI가 환경별로 정확히 존재하는가
  • 프록시가 X-Forwarded-Proto 를 전달하고, 앱이 이를 신뢰하는가
  • trailing slash 정책이 바뀌지 않았는가
  • 콜백 라우트가 리팩터링으로 변경되지 않았는가

6-2) CI에서 간단 검증 스크립트 예시

환경변수로 예상 redirect를 만들고, 콘솔 등록값(팀 문서/시크릿에 보관)을 비교하는 식으로 “사람 실수”를 줄일 수 있습니다.

node - <<'NODE'
const expected = process.env.EXPECTED_REDIRECT_URI
const actualBase = (process.env.PUBLIC_BASE_URL || '').replace(/\/$/, '')
const actual = `${actualBase}/api/auth/callback/google`

if (!expected) throw new Error('EXPECTED_REDIRECT_URI is required')
if (expected !== actual) {
  console.error('redirect_uri mismatch')
  console.error('expected:', expected)
  console.error('actual  :', actual)
  process.exit(1)
}
console.log('redirect_uri OK:', actual)
NODE

7) 그래도 안 되면: “Provider가 보는 값”을 서버 로그로 남기기

클라이언트가 Provider로 이동하기 직전, 서버에서 생성한 authorize URL을 로그로 남기면 원인 파악이 빨라집니다.

  • 생성된 authorize URL 전체
  • 그 중 redirect_uri 만 별도 로그
  • 배포 환경(도메인, 프록시 여부, 헤더)

단, 로그에 토큰/코드가 섞이지 않도록 authorize 단계까지만 기록하고, 운영에서는 마스킹 정책을 적용하세요.


마무리

redirect_uri_mismatch 는 복잡한 OAuth 지식보다도 “문자열이 1글자라도 다르면 실패”라는 사실을 중심으로 보면 빨리 해결됩니다. 가장 빠른 해결 루트는 다음 순서입니다.

  1. 브라우저에서 authorize 요청의 redirect_uri 를 추출해 디코딩
  2. Provider 콘솔 등록값과 완전 일치 비교(스킴/호스트/포트/경로/슬래시)
  3. 프록시 환경이면 X-Forwarded-Proto 반영 여부 확인
  4. Next.js/Spring Security에서 base URL을 명시적으로 고정

추가로 state 검증 실패나 401 루프까지 같이 겪고 있다면 아래 글에서 함께 정리해두었습니다.