Skip to content

ADR-25: OAuth 리턴 URL 처리

🇺🇸 English Version

날짜작성자리포지토리
2026-01-16@specvitalweb

컨텍스트

홈페이지 외의 페이지에서 GitHub OAuth 로그인 시 원래 페이지가 아닌 대시보드로 리다이렉트되는 문제 발생.

진입 페이지기대 동작실제 동작
/ko/pricing/ko/pricing으로 복귀/ko/dashboard로 리다이렉트
/ko/explore/ko/explore로 복귀/ko/dashboard로 리다이렉트
/ko/{owner}/{repo}분석 페이지로 복귀/ko/dashboard로 리다이렉트

근본 원인

1. 리다이렉트 로직 충돌

홈페이지의 AuthenticatedRedirect 컴포넌트가 전역 리다이렉트 동작에 영향.

2. 클라이언트-서버 스토리지 불일치

Client (브라우저)                   Server (Route Handler)
┌─────────────────┐                ┌──────────────────────┐
│ sessionStorage  │ ─────────X──── │ OAuth callback       │
│ (returnTo URL)  │  접근 불가    │ (code exchange)      │
└─────────────────┘                └──────────────────────┘

초기 구현은 sessionStoragereturnTo 저장, 하지만 Next.js App Router OAuth 콜백은 서버 사이드 Route Handler로 실행되어 브라우저 sessionStorage 접근 불가.

제약 조건

제약 조건출처영향
서버 사이드 OAuth 콜백Next.js App Router클라이언트 전용 스토리지 사용 불가
로케일 보존ADR-17리턴 URL에 로케일 접두사 포함 필수
보안 요구사항OAuth 모범 사례오픈 리다이렉트 공격 방지 필수

결정

쿠키 기반 리턴 URL 저장 및 서버 사이드 리다이렉트 검증 채택.

typescript
// OAuth 리다이렉트 전 (클라이언트 사이드)
const returnTo = window.location.pathname;
document.cookie = `returnTo=${encodeURIComponent(returnTo)}; path=/; max-age=300; SameSite=Lax`;
typescript
// OAuth 콜백 (서버 사이드 Route Handler)
const returnTo = cookies().get("returnTo")?.value;
const safeReturnTo = validateReturnUrl(returnTo);
cookies().delete("returnTo");
redirect(safeReturnTo || "/dashboard");

쿠키 설정

파라미터근거
저장소HTTP Cookie서버 사이드 접근 가능
Max-Age300초 (5분)OAuth 플로우 커버, 오래된 URL 위험 제한
SameSiteLax크로스 사이트 요청 공격 방지
Path/모든 라우트에서 사용 가능

검토한 옵션

옵션 A: sessionStorage 기반 (초기, 실패)

  • 브라우저 sessionStoragereturnTo 저장

장점:

  • 단순함, 쿠키 관리 불필요
  • 브라우저 탭 단위 스코프

단점:

  • 서버 사이드 Route Handler에서 sessionStorage 접근 불가
  • Next.js App Router와 아키텍처적 비호환

결정: 기각.

옵션 B: 쿠키 기반 저장 (채택)

  • OAuth 리다이렉트 전 HTTP 쿠키에 returnTo 저장
  • 콜백에서 서버가 쿠키 읽기, 검증, 삭제

장점:

  • 서버 사이드 Route Handler와 호환
  • 단기 수명 (5분)
  • SameSite=Lax로 CSRF 보호

단점:

  • 쿠키 관리 필요
  • 오픈 리다이렉트 공격 벡터 (검증으로 완화)

결정: 채택.

옵션 C: OAuth 상태 파라미터

  • OAuth 상태 파라미터에 returnTo 인코딩

장점:

  • 무상태, OAuth 스펙 내장

단점:

  • GitHub 상태 파라미터 크기 제한
  • CSRF 보호용으로 이미 사용 시 복잡도 증가

결정: 기각.

옵션 D: 데이터베이스 세션 저장

  • 세션 테이블에 returnTo 저장

장점:

  • 안정적인 서버 사이드 저장

단점:

  • 데이터베이스 지연 추가
  • 리다이렉트 URL 저장에 과도한 엔지니어링
  • PaaS 우선 단순성 위반 (ADR-06)

결정: 기각.

구현

리턴 URL 플로우

1. 사용자가 /ko/pricing에서 "GitHub으로 로그인" 클릭
   └─> 쿠키 설정: returnTo=/ko/pricing; max-age=300

2. GitHub OAuth로 리다이렉트
   └─> 사용자가 github.com에서 인증

3. GitHub이 /api/auth/callback/github으로 리다이렉트
   └─> 서버가 returnTo 쿠키 읽기
   └─> 검증: "/"로 시작하고 "//"가 아님
   └─> 쿠키 삭제
   └─> /ko/pricing으로 리다이렉트

4. 사용자가 /ko/pricing에 복귀 (인증됨)

URL 검증 로직

typescript
function validateReturnUrl(url: string | undefined): string | null {
  if (!url) return null;

  // 단일 슬래시로 시작 필수 (상대 경로)
  if (!url.startsWith("/")) return null;

  // 프로토콜 상대 URL 거부 (오픈 리다이렉트 벡터)
  if (url.startsWith("//")) return null;

  // 임베디드 자격 증명 URL 거부
  if (url.includes("@") || url.includes("\\")) return null;

  return url;
}

결과

긍정적:

  • OAuth 로그인 후 원래 페이지로 복귀
  • 로케일/i18n 컨텍스트 보존
  • Next.js App Router 서버 사이드 핸들러와 호환
  • 짧은 쿠키 만료 (5분)로 공격 윈도우 제한

부정적:

  • 쿠키 오버헤드 (최소, <100 바이트)
  • 오픈 리다이렉트 공격 표면 (검증으로 완화)
  • 쿠키 비활성화 시 대시보드로 폴백

보안 고려사항

오픈 리다이렉트 방지

공격 벡터완화 방법
절대 URL 삽입/ 접두사 필수
프로토콜 상대 URL// 접두사 거부
자격 증명 포함 URL@ 문자 거부
백슬래시 우회\ 문자 거부

쿠키 보안 속성

속성이점
SameSiteLax크로스 사이트 공격 방지
Max-Age300오래된 URL 위험 제한
Path/애플리케이션 스코프

참조

Open-source test coverage insights