ADR-25: OAuth 리턴 URL 처리
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2026-01-16 | @specvital | web |
컨텍스트
홈페이지 외의 페이지에서 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) │
└─────────────────┘ └──────────────────────┘초기 구현은 sessionStorage에 returnTo 저장, 하지만 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-Age | 300초 (5분) | OAuth 플로우 커버, 오래된 URL 위험 제한 |
| SameSite | Lax | 크로스 사이트 요청 공격 방지 |
| Path | / | 모든 라우트에서 사용 가능 |
검토한 옵션
옵션 A: sessionStorage 기반 (초기, 실패)
- 브라우저
sessionStorage에returnTo저장
장점:
- 단순함, 쿠키 관리 불필요
- 브라우저 탭 단위 스코프
단점:
- 서버 사이드 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 | @ 문자 거부 |
| 백슬래시 우회 | \ 문자 거부 |
쿠키 보안 속성
| 속성 | 값 | 이점 |
|---|---|---|
| SameSite | Lax | 크로스 사이트 공격 방지 |
| Max-Age | 300 | 오래된 URL 위험 제한 |
| Path | / | 애플리케이션 스코프 |
참조
- 커밋 9a961ed - OAuth 리턴 URL 수정
- ADR-17: next-intl i18n 전략
- Next.js Cookies API
