Published on

OAuth redirect_uri 불일치 400 원인·해결 7가지

Authors

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) 스킴 불일치: httphttps

가장 흔합니다. 로컬에서는 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.protocolhttp 로 고정되는 경우가 많으니, 이 설정이 핵심입니다.


2) 호스트 불일치: www 포함 여부, 서브도메인, 포트

app.example.comwww.example.com 은 완전히 다른 호스트입니다. 또한 :443 을 붙였는지 여부도 제공자에 따라 엄격히 비교합니다.

체크 포인트

  • www 유무
  • appapi 등 서브도메인
  • :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_DEV
  • OAUTH_REDIRECT_URI_STG
  • OAUTH_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가지는 실제로 한 글자라도 다르면 실패합니다.

  1. 스킴이 http 인지 https 인지
  2. 호스트가 www 포함인지, 서브도메인이 같은지
  3. 포트가 포함되는지(:443 포함 여부 포함)
  4. 경로의 trailing slash 유무
  5. 대소문자(일부 제공자는 엄격 비교)
  6. URL 인코딩이 이중으로 되었는지(%25 패턴이 보이면 의심)
  7. 제공자 콘솔에 등록된 앱(client) 자체가 맞는지

400 계열을 다루는 관점은 다른 사례에도 유용합니다. 비슷한 “요청은 맞는 것 같은데 제공자가 400을 주는” 유형의 문제 해결 흐름은 아래 글도 참고할 만합니다.


마무리: redirect_uri는 “문자열 완전 일치”로 다뤄라

redirect_uri mismatch 는 대부분 구현 실수라기보다 환경/인프라/인코딩/설정 분산 때문에 생깁니다. 해결의 핵심은 다음 두 가지입니다.

  • redirect_uri 를 동적으로 “추측해서” 만들지 말고, 환경변수로 고정하거나 프록시 헤더를 신뢰하도록 일관되게 구성
  • 제공자 콘솔 등록값과 실제 authorize 요청의 redirect_uri 를 나란히 놓고, 스킴/호스트/포트/경로/인코딩을 바이트 단위로 비교

위 7가지 원인을 순서대로 점검하면, 대부분의 400 redirect_uri 문제는 빠르게 끝낼 수 있습니다.