--- title: branch / feature-keycloak-token-mediating-access-handoff source_type: branch-note status: raw id: BR-KEYCLOAK-PATTERNS-OVERVIEW-009 kind: project-work-item project: keycloak-patterns-overview work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-009 inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1, DEC-KEYCLOAK-PATTERNS-OVERVIEW-TOKEN-MEDIATING-001@1] refines: [] overrides: [] depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-008] imports: [] delegates: [] accepts_delegations: [] contract_packet: 1 contract_packet_sha256: 76cd5b845d8e9bd8eb75e0a2f2c46adad06c6b67eb9bf358ada252205ad86f82 branch: feature-keycloak-token-mediating-access-handoff parent_branch: related_projects: [keycloak-patterns-overview] tags: [branch] created: 2026-07-24 target_merge: status_label: in-progress --- # branch: feature-keycloak-token-mediating-access-handoff ## 부모 (필수) - [[raw/project-notes/keycloak-patterns-overview]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: browser가 access token으로 API 200을 받고 refresh token은 network response에 존재하지 않는다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1` | done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | `WI-KEYCLOAK-PATTERNS-OVERVIEW-009` 완료 조건에 적용 | `[[raw/project-notes/keycloak-patterns-overview]]` | | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-TOKEN-MEDIATING-001@1` | AP2 backend가 token을 획득하고 access token만 browser에 전달한다 | `WI-KEYCLOAK-PATTERNS-OVERVIEW-009` 완료 조건에 적용 | `[[raw/project-notes/keycloak-patterns-overview]]` | ### 브랜치 지역 결정 | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ### 가져온 artifact 계약 | Artifact Ref | Owner | Producer | Schema Ref | |---|---|---|---| ## 가져온 프로젝트 계약 | Ref | Owner | 요약 | Branch 적용 | |---|---|---|---| ### 수신한 위임 | Delegation Ref | From | Concern | Status | |---|---|---|---| ### 가져온 흐름 단계 | Stage Ref | Order | Owner | Input | Action | Output | |---|---:|---|---|---|---| ## 목표 - `WI-KEYCLOAK-PATTERNS-OVERVIEW-009`의 완료 조건을 구현한다: browser가 access token으로 API 200을 받고 refresh token은 network response에 존재하지 않는다 ## 범위 ### 포함 범위 - backend 가 획득한 토큰 중 **access token 만 browser 로 handoff** → browser 가 Resource Server 를 **직접** 호출(proxy 없음) `200`. - **refresh token 이 browser 로 가는 network response(응답 바디/헤더/쿠키)에 부재**함을 확인(refresh 는 backend 만 보유). - 위 두 가지를 로컬 스택에서 네트워크 탭/응답 검사로 재현·검증. ### 제외 범위 > 의도적으로 제외. 면접 등에서 "이건 범위에 없었습니다" 근거. - **backend 의 code→token 교환 + refresh 서버측 보관 자체** → `WI-008`(`feature-keycloak-token-mediating-confidential-client`) 소유. 본 branch 는 그 산출물(서버측 보관 access)을 *browser 로 넘기는 경계*만. - BFF(모든 요청 proxy) → AP3. 본 패턴은 proxy **없이** access 를 browser 에 위임(TMB 의 정의적 차이). - project decision registry 변경. ## 근거 (필수, 최소 1개+) | Source | 정당화하는 결정 | |---|---| | `[[raw/official-docs/oauth2-browser-based-apps-ietf-draft]]` | D1·D2 — Token-Mediating Backend 정의: backend 가 confidential client 로 토큰 획득 후 **access token 을 앱에 건네 RS 와 직접 통신**(`OAUTH-BBA-C2`), BFF 대비 proxy 불필요·보안수준 낮음(`OAUTH-BBA-C5`), BFF 는 어떤 토큰도 browser 미노출(`OAUTH-BBA-C1`, 대비 근거) | | `[[raw/company-tech-blogs/curity-bff-pattern-spa]]` | D2 — token 을 browser 에 두는 것의 위험(대비 사례, company-case-study — 단독 official 단언 불가) | | `[[raw/branch-notes/feature-keycloak-token-mediating-confidential-client]]` | 의존 — WI-008 이 서버측에 보관한 access/refresh 를 consume | ## TODO - [ ] backend 가 access token 만 browser 로 전달하는 handoff 경로 구현 — 등급: `planned` - [ ] browser 가 그 access token 으로 `/api` `200`(RS 직접 호출) — 등급: `planned` - [ ] refresh token 이 browser network response 에 부재함을 네트워크 탭/응답 바디로 확인 — 등급: `planned` ## 진행 중 메모 - 본 branch 는 WI-008(획득·서버보관)에 **의존**하며, "browser 로 무엇을 넘기는가"의 경계만 결정. AP2 구현 코드 미존재(`NO_GROUND_TRUTH`) → 전부 `planned`. ## 결정 사항 - 2026-07-24: **D1** access token 만 browser 로 handoff, browser 가 RS 를 직접 호출(proxy 없음) / 이유: TMB 정의(backend 가 획득하되 access 를 앱에 위임) — BFF 대비 경량 / 대안: BFF(AP3, 모든 API proxy·browser 토큰 0개) 또는 browser-client(AP1, browser 가 OAuth 전담) / 근거: `[[raw/official-docs/oauth2-browser-based-apps-ietf-draft]]` - 2026-07-24: **D2** refresh token 은 browser 로 가는 response 에 포함하지 않음(backend 만 보유) / 이유: refresh 노출 시 XSS 1건=장기 세션 탈취, TMB 는 browser-client 보다 안전한 지점이 바로 이것 / 대안: browser 에 refresh 도 전달(browser-client 로 강등) / 근거: `[[raw/official-docs/oauth2-browser-based-apps-ietf-draft]]`(`OAUTH-BBA-C1`·`C2` 대비) - 2026-07-24: **D3**(상속 `DEC-…-ACCEPTANCE-001@1`) 완료 판정 = browser `/api` `200` **AND** refresh 가 network 에 부재 / 본 branch 적용점: 두 조건 동시 충족만 done ## Decision Evidence Map / 결정-근거 매핑 > `official-standard`(IETF draft) 우선, `company-case-study`(Curity)는 보조(단독 official 단언 금지). IETF 는 일반 패턴 정의 → project 매핑은 Open Risk. | Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | access token 만 browser 로 handoff, browser 가 RS 직접 호출(proxy 없음) | TMB(AP2) → 이 결정 / BFF(AP3, 전 요청 proxy) 또는 browser-client(AP1) → 대안 | `raw/official-docs/oauth2-browser-based-apps-ietf-draft.md#OAUTH-BBA-C2`, `#OAUTH-BBA-C5` | `official-standard` | draft 는 "access 를 앱에 제공"만 서술 — **handoff 메커니즘**(응답 바디 JSON / readable cookie / 전용 엔드포인트)은 규정 안 함 → 구현 결정 | | D2 | refresh token 을 browser response 에 미포함(backend 만 보유) | TMB(refresh 서버측) → 이 결정 / browser-client(refresh 브라우저) → 대안 | `raw/official-docs/oauth2-browser-based-apps-ietf-draft.md#OAUTH-BBA-C1`, `#OAUTH-BBA-C2`, `raw/company-tech-blogs/curity-bff-pattern-spa.md#CURITY-BFF-C6`, `[[raw/branch-notes/feature-keycloak-token-mediating-confidential-client]]` D2 | `official-standard + company-case-study(보조)` | draft 는 refresh 부재를 *직접* 문장화하지 않고 "access 만 제공"에서 함의 — refresh 부재 *보장 방법*(응답에서 제외)은 구현 결정. refresh 노출의 *why*(탈취 시 유효기간 내내 데이터 접근)는 `CURITY-BFF-C6` 이 pin(company-case-study 보조 — 단독 official 단언 불가) | | D3 | 완료 판정 = browser `/api` `200` AND refresh network 부재 (상속 acceptance) | 항상 / N/A | `raw/project-notes/keycloak-patterns-overview.md` (`DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1`, 상속) | `project-decision(inherited)` | "network 부재"의 검사 범위(응답 바디만 vs 헤더·쿠키 포함)를 명시적으로 정의해야 함 | ## 구현 가이드 > **Trace**: D1(`OAUTH-BBA-C2/C5`) + D2(`OAUTH-BBA-C1/C2` + WI-008 D2) + D3(inherited acceptance). AP2 코드 부재(`NO_GROUND_TRUTH`)라 사전 명세이며 등급 `planned`. > > - **UNSUPPORTED_IMPL_DECISION**: (a) **handoff 메커니즘** — backend 가 access 를 browser 에 전달하는 방식(로그인 후 backend 엔드포인트가 access 를 JSON 으로 반환 / JS-readable 쿠키 / postMessage 등). IETF draft 는 "제공한다"만 서술, 방식 미규정. trade-off: JSON 반환은 단순하나 access 를 JS 가 읽어 XSS 노출면 존재(TMB 의 알려진 한계), readable cookie 도 유사. **근거 없음 → 착수 시 결정**. (b) **refresh 부재 검사 범위** — 응답 바디만 vs 헤더/Set-Cookie 전수. trade-off: 바디-only 는 간단하나 Set-Cookie 경유 누출을 놓침 / 전수는 안전(누출 지점 전부 커버)하나 검사 비용↑. **본 branch 는 전수 검사로 수렴**(아래 refresh 격리 row · §Claims To Verify 와 정합) — 잔여 미결 아님. | 항목 | 사전 명세 (planned) | Trace | |---|---|---| | Handoff endpoint | 로그인 완료 후 backend 가 access token(만)을 browser 에 반환하는 경로 | D1 · UNSUPPORTED_IMPL_DECISION(a) | | Browser→RS | browser 가 받은 access 로 `Authorization: Bearer` 붙여 RS `/api` 직접 호출 `200` | D1(`OAUTH-BBA-C2`) | | refresh 격리 | handoff 응답(바디·헤더·쿠키)에서 refresh 제외 — refresh 는 WI-008 서버측 보관에만 존재 | D2 · UNSUPPORTED_IMPL_DECISION(b) | ## 엣지·실패·의존 - **실패·엣지 경로**: - browser 의 access token 만료 → browser 가 backend handoff 엔드포인트에서 재수령(backend 가 WI-008 D2 로 자동 refresh 후 새 access 발급). refresh 자체 만료 시 재로그인. - handoff 응답에 refresh 가 실수로 포함 → 완료조건 위반(D3). 검증 대상. - **다른 계약 의존**: - `WI-KEYCLOAK-PATTERNS-OVERVIEW-008`(`feature-keycloak-token-mediating-confidential-client`) D1·D2 — 서버측 토큰 획득·보관을 consume. 그 계약이 바뀌면(저장소·refresh 정책) 본 branch handoff 영향. 특히 **handoff 엔드포인트가 browser 를 인증하는 방식**(세션 쿠키 vs 기타)은 WI-008 의 세션모델(`oauth2Login()` 세션 vs 수동 grant — WI-008 §구현 가이드 `UNSUPPORTED_IMPL_DECISION(a)`)에 커플링. WI-008 이 그 축을 정하면 본 branch handoff 인증도 결정된다. - `WI-KEYCLOAK-PATTERNS-OVERVIEW-004`(`feature-keycloak-spring-rs-audience-validator`) D1·D6 — browser→RS `200` 이 소비하는 **Resource Server 의 baseline JWT 검증**(`issuer-uri`/JWKS discovery = D6, signature·`iss`·`exp`·`aud` = D1)을 consume. AP2 가 이 AP1 RS 를 재사용하는지 AP2 backend 가 RS 를 겸직하는지는 착수 시 결정(`planned`). AP2 done-bar(D3)의 signature 함정은 refresh-부재 축이라 `aud` 검증은 본 branch 완료조건에 비필수(RS 소유 브랜치의 관심사). ## 검증해야 할 주장 / Claims To Verify > IETF draft 는 패턴 정의(메커니즘 근거)지만 본 프로젝트 실제 동작을 자동 보장하지 않는다. AP2 코드 미구현이라 전부 구현 후 검증. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | browser 가 backend 로부터 access token(만) 수령 | handoff 메커니즘 미결(UNSUPPORTED_IMPL_DECISION a) | 로그인 후 handoff 응답에 access 존재 + refresh 부재 확인 | `planned` | | browser 가 그 access 로 RS `/api` `200`(직접 호출) | AP2 코드 미구현 | 네트워크 탭에서 browser→RS 직접 요청 + `200` 확인 | `planned` | | refresh token 이 browser 로 가는 network response 에 부재 | 부재 보장 방법(응답 제외) 미구현 · 검사 범위 미정(b) | 네트워크 탭 응답 바디·헤더·Set-Cookie 전수 검사에서 refresh 부재 확인 | `planned` | ## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때) `/coverage` 실행 전. ## 마주친 문제 아직 없음. ## 묶음 (이 branch에서 파생된 자료) ## 관련 일일 노트 해당 없음. ## 완료 후 정리 - PR 링크: - 리뷰 메모: