203 lines
13 KiB
Markdown
203 lines
13 KiB
Markdown
---
|
|
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
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- [[raw/project-notes/keycloak-patterns-overview]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: browser가 access token으로 API 200을 받고 refresh token은 network response에 존재하지 않는다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| 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]]` |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- GENERATED: artifact-imports:start -->
|
|
### 가져온 artifact 계약
|
|
|
|
| Artifact Ref | Owner | Producer | Schema Ref |
|
|
|---|---|---|---|
|
|
<!-- GENERATED: artifact-imports:end -->
|
|
|
|
<!-- GENERATED: project-contract-imports:start -->
|
|
## 가져온 프로젝트 계약
|
|
|
|
| Ref | Owner | 요약 | Branch 적용 |
|
|
|---|---|---|---|
|
|
<!-- GENERATED: project-contract-imports:end -->
|
|
|
|
<!-- GENERATED: received-delegations:start -->
|
|
### 수신한 위임
|
|
|
|
| Delegation Ref | From | Concern | Status |
|
|
|---|---|---|---|
|
|
<!-- GENERATED: received-delegations:end -->
|
|
|
|
<!-- GENERATED: flow:start -->
|
|
### 가져온 흐름 단계
|
|
|
|
| Stage Ref | Order | Owner | Input | Action | Output |
|
|
|---|---:|---|---|---|---|
|
|
<!-- GENERATED: flow:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
- `WI-KEYCLOAK-PATTERNS-OVERVIEW-009`의 완료 조건을 구현한다: browser가 access token으로 API 200을 받고 refresh token은 network response에 존재하지 않는다
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- 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
|
|
|
|
<!-- section-id: decision-evidence -->
|
|
## 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 헤더·쿠키 포함)를 명시적으로 정의해야 함 |
|
|
|
|
<!-- section-id: implementation -->
|
|
## 구현 가이드
|
|
|
|
> **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) |
|
|
|
|
<!-- section-id: edge-failure-dependency -->
|
|
## 엣지·실패·의존
|
|
|
|
- **실패·엣지 경로**:
|
|
- 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 소유 브랜치의 관심사).
|
|
|
|
<!-- section-id: claims-to-verify -->
|
|
## 검증해야 할 주장 / 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에서 파생된 자료)
|
|
|
|
<!-- GENERATED: branches:start -->
|
|
<!-- GENERATED: branches:end -->
|
|
|
|
## 관련 일일 노트
|
|
|
|
해당 없음.
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|