---
title: "📓 작업일지 — 앱 v1.0.3 OAuth 봉인 연동(콜백 park 방식, 딥링크 토큰 폐지)"
category: "worklog"
parent: "Claude Code 작업일지"
updated: "2026-07-13"
priority: "High"
purpose: "카카오·네이버 앱 로그인 콜백이 access/refresh 토큰을 커스텀 스킴 딥링크(kr.dallog.app://)에 그대로 실어 보내던 방식을, 워커에 토큰을 봉인(park)하고 딥링크엔 1회용 봉인문(otc)만 전달한 뒤 앱이 PKCE verifier로 회수(redeem)하는 방식으로 바꿔, 딥링크 가로채기로는 토큰을 얻을 수 없도록 한 보안 강화 기록(demo 0f60dfd 선검증분 main 반영)."
read_when: ["앱 OAuth", "native_ state", "park redeem", "native_auth_once", "딥링크 토큰 탈취", "PKCE 콜백"]
document_type: "개발일지"
source_status: "generated"
knowledge_group: "03_history"
work_timestamp: "20260713_154828"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
context: "달록 본레포(ccy-hansbridge/dallog), 커밋 d8fa0d6(2026-07-13, demo-dallog 0f60dfd 선검증분 main 반영)."
---

# 작업일지 — 앱 v1.0.3 OAuth 봉인 연동(콜백 park 방식, 딥링크 토큰 폐지)

> 앱(Capacitor) 로그인은 카카오·네이버 웹 콜백에서 토큰을 받아 커스텀 스킴 딥링크(`kr.dallog.app://auth/callback#access_token=...`)로 앱에 넘겨주는 방식이었다. 그런데 커스텀 스킴은 다른 앱이 같은 스킴을 등록해 가로챌 수 있어(RFC 8252), 토큰이 딥링크에 그대로 실리면 탈취 위험이 있다. 이번 커밋으로 토큰을 딥링크에 직접 싣지 않고, 워커에 잠깐 "봉인"해 두었다가 앱이 PKCE verifier로 증명해야만 꺼내갈 수 있게 바꿨다.

## 1. 무엇을 했나

### 흐름 변경 — park/redeem
- **park(봉인)**: 웹 콜백(`KakaoCallbackPage.tsx`/`NaverCallbackPage.tsx`)이 토큰 교환 후, `native_` state일 경우 토큰을 직접 딥링크에 싣는 대신 워커 `/native/park`에 `{state, access_token, refresh_token}`을 POST. 워커가 AES-GCM으로 봉인(120초 TTL)해 봉인문(`otc`)을 반환.
- **딥링크**: `kr.dallog.app://auth/callback#otc=...&state=...` — 베어러 토큰 없이 봉인문만 전달.
- **redeem(회수)**: 앱이 `/native/redeem`에 `{otc, verifier}` 제출 → `SHA-256(verifier)`가 state 내 PKCE challenge와 일치할 때만 토큰 반환. verifier는 앱 WebView에만 저장돼 있어, 딥링크만 가로챈 공격자는 회수 불가.

### 1회용 재사용 차단 — native_auth_once 테이블
- 신규 마이그레이션 `migrations/2026-07-13_native_auth_once.sql`: `native_auth_once(key_hash primary key, kind, created_at)` 테이블 신설. RLS 활성화, `anon`·`authenticated` 권한 회수(워커 service_role 전용). 원문(state/otc) 대신 `SHA-256(kind|원문)` 해시만 저장.
- `park` 호출 시 state를 1회용으로 기록(first-write-wins) — 같은 state로 park를 다시 부르면 거부. 딥링크를 가로챈 앱이 피해자 state에 공격자 토큰을 다시 봉인해 세션을 바꿔치기하는 공격(Codex 3회차 치명 지적) 차단.
- `redeem` 성공 후 otc도 1회용으로 소각 — 단, verifier 검증 통과 후에만 소각해 오verifier 시도로 정상 otc가 먼저 소각되는 DoS를 방지.
- 저장 실패 시 fail-closed(거부).

### 봉인 키
- `NATIVE_SEAL_KEY` 시크릿이 있으면 그것을, 없으면 기존 `SUPABASE_SERVICE_ROLE_KEY`에서 도메인 분리 접두사(`dallog-native-seal|`)를 붙여 SHA-256으로 단방향 파생해 사용 — 신규 시크릿 등록 없이도 동작하며 서비스롤 키 자체는 노출·역산되지 않음.

### CORS/Origin
- `/native/redeem`에 한해 앱 WebView Origin(`https://localhost`, `capacitor://localhost`)을 추가 허용 — 그 외 라우트는 기존 웹 allowlist(`ALLOWED_ORIGINS`) 그대로.

### 콜백 페이지 부가 수정
- 카카오 exchange 시 `redirect_uri`를 `state?.startsWith('native_')`면 `https://dallog.kr/auth/kakao/callback`으로 고정(앱은 authorize가 dallog.kr 고정이라 exchange도 동일 값이어야 함 — env 불일치 방지).
- 카카오/네이버 콜백 에러 메시지를 "CF Workers auth-proxy가 아직 배포되지 않았어요" 같은 내부 구현 노출 문구에서 "지금 사용할 수 없어요, 다른 로그인을 이용해 주세요" 형태의 사용자向 문구로 교체.

## 2. 주요 변경 파일

| 파일 | 내용 |
|---|---|
| `migrations/2026-07-13_native_auth_once.sql` | 1회용 재사용 차단 테이블 신설(+11줄) |
| `src/pages/KakaoCallbackPage.tsx` | park 호출 + otc 딥링크 전달, redirect_uri 고정, 에러 문구 정리 |
| `src/pages/NaverCallbackPage.tsx` | 위와 동일한 park 흐름 반영 |
| `workers/auth-proxy/index.js` | `/native/park`·`/native/redeem` 신설(+161줄), 봉인/해제·1회용 기록·CORS 확장 |

### 핵심 발췌 — 딥링크 전달 방식 변경(카카오 콜백)
```diff
- window.location.href = `kr.dallog.app://auth/callback#access_token=${data.access_token}&refresh_token=${data.refresh_token || ''}`
+ const parkRes = await fetch(`${authProxy}/native/park`, { method: 'POST', ... })
+ const park = await parkRes.json().catch(() => null)
+ if (!parkRes.ok || !park?.otc) throw new Error(park?.error?.message || '앱 전달 준비 실패')
+ window.location.href = `kr.dallog.app://auth/callback#otc=${encodeURIComponent(park.otc)}&state=${encodeURIComponent(state)}`
```

## 3. 관련 커밋

- `d8fa0d6` fix(auth): 앱 v1.0.3 OAuth 봉인 연동 — 콜백 2페이지 park 방식(딥링크 토큰 폐지)+워커 1회용 차단+마이그레이션 등재(demo 0f60dfd 선검증분) (2026-07-13)

## 4. 남은 이슈

- 커밋 메시지 기준: demo-dallog 커밋 `0f60dfd`에서 먼저 선검증된 뒤 main에 반영된 것으로 명시됨(배포 거버넌스 — 데모 선검증 원칙 준수). 그 외 별도 남은 이슈 언급 없음.
