- Published on
OAuth 콜백 400 redirect_uri_mismatch 즉시 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
OAuth 로그인 연동을 붙이다 보면, 인증 화면까지는 잘 갔다가 콜백에서 갑자기 400 과 함께 redirect_uri_mismatch 가 터지는 경우가 가장 흔합니다. 이 에러는 “내가 보낸 redirect_uri 가 Provider에 등록된 값과 정확히 일치하지 않는다”는 뜻인데, 문제는 어디가 어떻게 다른지가 로그 한 줄로는 잘 안 보인다는 점입니다.
이 글은 콜백 400/redirect_uri_mismatch 를 즉시 해결하기 위한 실무형 가이드입니다. Provider 콘솔 설정부터, 프록시/로드밸런서 뒤에서 스킴이 바뀌는 문제, Next.js 및 Spring Security에서 실제로 어떤 값이 redirect_uri 로 나가는지까지 빠르게 점검합니다.
관련해서 state 와 redirect_uri 로 401 루프가 생기는 케이스도 자주 같이 나오니, 필요하면 아래 글도 함께 보시면 좋습니다.
1) 에러의 본질: “문자열 완전 일치” 문제
대부분의 OAuth Provider는 redirect_uri 를 문자열 완전 일치(exact match) 로 비교합니다. 즉, 아래 중 하나라도 다르면 mismatch 입니다.
- 스킴:
httpvshttps - 호스트:
example.comvswww.example.comvsapi.example.com - 포트:
:80,:443,:3000등 - 경로:
/oauth/callbackvs/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_uri가http로 생성되어 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.com 과 www.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_URL을https://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글자라도 다르면 실패”라는 사실을 중심으로 보면 빨리 해결됩니다. 가장 빠른 해결 루트는 다음 순서입니다.
- 브라우저에서 authorize 요청의
redirect_uri를 추출해 디코딩 - Provider 콘솔 등록값과 완전 일치 비교(스킴/호스트/포트/경로/슬래시)
- 프록시 환경이면
X-Forwarded-Proto반영 여부 확인 - Next.js/Spring Security에서 base URL을 명시적으로 고정
추가로 state 검증 실패나 401 루프까지 같이 겪고 있다면 아래 글에서 함께 정리해두었습니다.