{ "version": "1.1", "id": "ap1-direct-architecture", "title": "AP1은 OAuth client와 token custody를 브라우저에 둔다", "question": "AP1에서 code 교환, token 보관, Resource Server JWT 입력은 어느 구성 요소에 놓이는가?", "type": "architecture", "direction": "LR", "audience": [ "SPA OAuth와 Resource Server 경계를 설계하는 개발자" ], "summary": "Public SPA가 PKCE code 교환을 수행하고 token set을 JavaScript memory에 보관한 뒤 access JWT를 Resource Server 입력으로 사용한다.", "alt": "SPA, Keycloak, 브라우저 JavaScript memory, Resource Server가 왼쪽에서 오른쪽으로 연결된 AP1 직접 인증 아키텍처.", "long_description": "왼쪽의 public SPA가 Keycloak과 Authorization Code 및 PKCE S256 계약을 수행한다. Keycloak token 응답의 access, refresh, ID token은 브라우저 JavaScript memory에 놓이며, 그중 access JWT가 오른쪽 Resource Server의 검증 입력이 된다. Resource Server는 issuer, 시간 제약과 keycloak-pattern-api audience를 검증한다.", "source_context": { "document": "document.md", "document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371", "anchor": { "kind": "marker", "value": "ap1-direct-architecture", "line": 152 } }, "composition": { "profile": "component-flow", "diagram_only": true, "reference_ids": [ "payment-event-flow" ], "rationale": "AP1의 핵심은 SPA에서 시작한 code와 token이 브라우저 memory를 거쳐 Resource Server JWT 입력으로 이동하는 방향성 있는 구성 요소 경로다.", "focus_node": "browser-spa" }, "groups": [], "nodes": [ { "id": "browser-spa", "label": "Public SPA", "kind": "service", "role": "source", "emphasis": "primary", "description": "Authorization Code와 PKCE를 직접 다루고 access JWT로 Resource Server를 호출하는 public OAuth client.", "details": [ "spa-public", "PKCE S256" ], "evidence": [ { "start_line": 144, "end_line": 148 } ], "assumption": false }, { "id": "browser-token-memory", "label": "Browser JS memory", "kind": "database", "role": "store", "shape": "database", "description": "Access, refresh, ID token을 실행 중 보관하며 reload 뒤에는 복구하지 않는 browser-side custody.", "details": [ "access · refresh · ID token", "persistent Web Storage 없음" ], "evidence": [ { "start_line": 148, "end_line": 150 } ], "assumption": false }, { "id": "keycloak", "label": "Keycloak", "kind": "service", "role": "service", "description": "SPA의 code 교환 상대이며 API가 검증하는 JWT 서명의 출처.", "evidence": [ { "start_line": 144, "end_line": 146 } ], "assumption": false }, { "id": "api-resource-server", "label": "Resource Server", "kind": "service", "role": "sink", "description": "Access JWT의 issuer, 시간 제약과 audience를 검증하는 API.", "details": [ "issuer · time", "keycloak-pattern-api audience" ], "evidence": [ { "start_line": 144, "end_line": 150 } ], "assumption": false } ], "edges": [ { "id": "spa-to-keycloak", "from": "browser-spa", "to": "keycloak", "label": "Authorization Code + PKCE S256", "kind": "request", "evidence": [ { "start_line": 144, "end_line": 146 } ], "assumption": false }, { "id": "keycloak-to-spa", "from": "keycloak", "to": "browser-spa", "label": "access · refresh · ID token", "kind": "response", "style": "dashed", "evidence": [ { "start_line": 144, "end_line": 148 } ], "assumption": false }, { "id": "spa-to-memory", "from": "browser-spa", "to": "browser-token-memory", "label": "token set 보관", "kind": "data", "evidence": [ { "start_line": 148, "end_line": 150 } ], "assumption": false }, { "id": "spa-to-resource-server", "from": "browser-spa", "to": "api-resource-server", "label": "memory-held access JWT · Bearer", "kind": "request", "emphasis": "primary", "evidence": [ { "start_line": 144, "end_line": 150 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "로그인 message 순서가 아니라 OAuth client, token custody, JWT 검증 위치를 한 수준의 component path로 표현했다." } }