- Published on
OAuth redirect_uri 불일치 400 원인·해결 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
OAuth 인증을 붙이다 보면, 로그인 버튼을 눌렀을 뿐인데 제공자(Authorization Server)에서 400 과 함께 redirect_uri mismatch 류의 에러를 돌려주는 상황을 자주 만납니다. 이 에러는 단순히 “URI가 다르다”가 아니라, 제공자 콘솔에 등록된 값과 실제 authorize 요청에 포함된 redirect_uri 가 바이트 단위로 동일하지 않다는 뜻인 경우가 대부분입니다.
문제는 개발 환경, 프록시, 로드밸런서, 프레임워크 기본값, URL 인코딩 등 변수가 많아서 “분명 맞게 넣었는데 왜 틀리다고 하지?”가 흔하다는 점입니다. 아래는 실무에서 가장 많이 터지는 7가지 원인과 해결책입니다.
관련해서 프록시 환경에서 특히 자주 발생하는 케이스는 별도로 정리해 둔 글도 참고하세요.
문제를 재현 가능한 형태로 먼저 고정하기
가장 먼저 해야 할 일은 실제 authorize 요청 URL을 정확히 확보하는 것입니다.
- 브라우저 네트워크 탭에서 authorize 요청 URL 복사
- 서버 로그에서
redirect_uri파라미터 값 출력 - 가능하면 제공자 에러 응답의
error_description도 함께 저장
예시(형태만 참고):
curl -v "https://auth.example.com/oauth2/authorize?response_type=code&client_id=abc&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback&scope=openid"
이때 이후 모든 비교는 제공자 콘솔에 등록된 리다이렉트 URI 와, 위 요청에 실제로 들어간 redirect_uri 를 기준으로 진행합니다.
1) 스킴 불일치: http 와 https
가장 흔합니다. 로컬에서는 http://localhost:3000/callback 로 잘 되다가, 배포 후에는 https:// 로 바뀌거나 반대로 내부 통신은 http 인데 외부는 https 인 경우가 많습니다.
증상
- 제공자 콘솔에는
https://app.example.com/oauth/callback로 등록 - 실제 요청은
http://app.example.com/oauth/callback로 나감
해결
- 제공자 콘솔의 Redirect URI를 실제 외부 스킴에 맞춤
- 애플리케이션이
redirect_uri를 동적으로 만들고 있다면, 외부 스킴을 신뢰할 수 있게X-Forwarded-Proto를 반영
Node/Express에서 프록시 뒤 스킴을 신뢰하도록 설정 예시:
import express from "express";
const app = express();
app.set("trust proxy", true);
app.get("/login", (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const redirectUri = `${baseUrl}/oauth/callback`;
res.send(redirectUri);
});
프록시 뒤에서 req.protocol 이 http 로 고정되는 경우가 많으니, 이 설정이 핵심입니다.
2) 호스트 불일치: www 포함 여부, 서브도메인, 포트
app.example.com 과 www.example.com 은 완전히 다른 호스트입니다. 또한 :443 을 붙였는지 여부도 제공자에 따라 엄격히 비교합니다.
체크 포인트
www유무app과api등 서브도메인:3000같은 포트 포함 여부
해결
- 제공자 콘솔에 실제로 authorize 요청에 들어가는 호스트/포트 그대로 등록
- 프론트에서 임의로
window.location.origin을 쓰는 경우, 배포 도메인 변형(예: CDN 도메인) 때문에 달라질 수 있으니 주의
프론트에서 redirect_uri 를 만들 때 안전하게 환경변수로 고정하는 예시:
// Next.js
const redirectUri = process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI;
if (!redirectUri) throw new Error("NEXT_PUBLIC_OAUTH_REDIRECT_URI is required");
3) 경로(path) 불일치: 슬래시 하나 차이
/oauth/callback 과 /oauth/callback/ 은 다른 URL입니다. 제공자 대부분이 문자열 완전 일치를 요구합니다.
자주 터지는 패턴
- 콘솔에는
/oauth/callback로 등록 - 실제 요청은
/oauth/callback/로 나감
또는 라우팅 리라이트로 인해 실제 콜백 엔드포인트가 /api/auth/callback/provider 인데, 콘솔에는 /auth/callback/provider 로 등록한 경우도 흔합니다.
해결
- 콜백 엔드포인트를 먼저 확정하고, 콘솔 등록값을 그에 맞게 수정
- 애플리케이션에서 URL 조합 시 슬래시 중복/누락을 방지
URL 조합 실수 방지 예시:
function joinUrl(base, path) {
return new URL(path, base).toString();
}
const redirectUri = joinUrl("https://app.example.com", "/oauth/callback");
4) URL 인코딩/디코딩 불일치: 이중 인코딩
redirect_uri 는 쿼리 파라미터로 전달되므로 인코딩이 필요합니다. 하지만 라이브러리가 자동 인코딩을 하는데 개발자가 또 인코딩하면 이중 인코딩이 되어 mismatch가 발생합니다.
예시
- 기대값:
https://app.example.com/oauth/callback - 정상 인코딩:
https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback - 이중 인코딩:
https%253A%252F%252Fapp.example.com%252Foauth%252Fcallback
해결
- authorize URL을 만들 때 인코딩 책임을 한 곳으로 모으기
- 라이브러리 사용 시
redirect_uri에는 “원문 URL” 을 넣고, 라이브러리가 인코딩하도록 두기
직접 쿼리를 조립할 때 안전한 예시:
const params = new URLSearchParams({
response_type: "code",
client_id: process.env.CLIENT_ID,
redirect_uri: "https://app.example.com/oauth/callback",
scope: "openid profile",
});
const authorizeUrl = `https://auth.example.com/oauth2/authorize?${params.toString()}`;
5) 프록시/로드밸런서 뒤에서 외부 URL을 내부 URL로 인식
서비스는 https://app.example.com 으로 접속하지만, 애플리케이션은 내부에서 http://service:8080 으로 요청을 받는 구조에서 자주 발생합니다.
이때 서버가 redirect_uri 를 “현재 요청 기준”으로 만들면 내부 호스트로 생성되어 mismatch가 납니다.
해결
X-Forwarded-Proto,X-Forwarded-Host를 신뢰하도록 서버/프레임워크 설정- Nginx/Ingress에서 해당 헤더를 올바르게 전달
Nginx에서 전달 헤더 설정 예시:
location / {
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;
proxy_pass http://app_upstream;
}
이 주제는 케이스가 다양하므로, 프록시 환경 중심의 상세 가이드는 아래 글이 더 도움이 됩니다.
6) 환경별 Redirect URI 등록 누락: dev/stage/prod
팀에서 흔히 겪는 실수는 “로컬은 되는데 스테이징만 안 됨” 입니다. 원인은 단순히 제공자 콘솔에 스테이징 Redirect URI를 등록하지 않았거나, 다른 앱(client) 설정에 등록했기 때문입니다.
해결 체크리스트
- 제공자 콘솔에서
client_id가 맞는지 확인 - 앱(클라이언트) 별로 Redirect URI 목록이 분리되어 있는지 확인
- 스테이징 도메인 전체가 바뀌었는데 예전 값을 그대로 두지 않았는지 확인
실무 팁: 환경별로 명시적으로 분리하세요.
OAUTH_REDIRECT_URI_DEVOAUTH_REDIRECT_URI_STGOAUTH_REDIRECT_URI_PROD
7) 제공자 정책: 와일드카드 금지, 쿼리 포함/제외 규칙
제공자마다 Redirect URI 허용 정책이 다릅니다.
- 와일드카드(예:
https://app.example.com/*)를 금지 - 쿼리 스트링이 포함된 Redirect URI를 금지하거나, 반대로 “등록된 값과 쿼리까지 완전 일치”를 요구
localhost허용 범위 제한
해결
- 제공자 문서에서 Redirect URI 정책을 먼저 확인
- 가능한 한 Redirect URI는 고정 경로로 두고, 상태값은
state로 전달
state 를 사용해 콜백 이후 원래 이동할 페이지를 복원하는 예시:
import crypto from "crypto";
function makeState(nextPath) {
const nonce = crypto.randomBytes(16).toString("hex");
return Buffer.from(JSON.stringify({ nonce, nextPath })).toString("base64url");
}
const state = makeState("/settings");
Redirect URI에 ?next=... 를 덕지덕지 붙여 등록값과 달라지게 만드는 대신, state 로 안전하게 운반하는 방식이 mismatch를 크게 줄입니다.
빠른 진단: 5분 안에 확인할 체크리스트
아래 7가지는 실제로 한 글자라도 다르면 실패합니다.
- 스킴이
http인지https인지 - 호스트가
www포함인지, 서브도메인이 같은지 - 포트가 포함되는지(
:443포함 여부 포함) - 경로의 trailing slash 유무
- 대소문자(일부 제공자는 엄격 비교)
- URL 인코딩이 이중으로 되었는지(
%25패턴이 보이면 의심) - 제공자 콘솔에 등록된 앱(client) 자체가 맞는지
400 계열을 다루는 관점은 다른 사례에도 유용합니다. 비슷한 “요청은 맞는 것 같은데 제공자가 400을 주는” 유형의 문제 해결 흐름은 아래 글도 참고할 만합니다.
마무리: redirect_uri는 “문자열 완전 일치”로 다뤄라
redirect_uri mismatch 는 대부분 구현 실수라기보다 환경/인프라/인코딩/설정 분산 때문에 생깁니다. 해결의 핵심은 다음 두 가지입니다.
redirect_uri를 동적으로 “추측해서” 만들지 말고, 환경변수로 고정하거나 프록시 헤더를 신뢰하도록 일관되게 구성- 제공자 콘솔 등록값과 실제 authorize 요청의
redirect_uri를 나란히 놓고, 스킴/호스트/포트/경로/인코딩을 바이트 단위로 비교
위 7가지 원인을 순서대로 점검하면, 대부분의 400 redirect_uri 문제는 빠르게 끝낼 수 있습니다.