{ "version": "1.1", "id": "ap2-mediator-architecture", "title": "AP2는 refresh custody와 API caller를 서로 다른 경계에 둔다", "question": "AP2에서 server-held refresh token과 browser-held access token은 어떻게 Resource Server 직접 호출로 이어지는가?", "type": "architecture", "direction": "LR", "audience": [ "Token mediator의 상태와 브라우저 API 계약을 설계하는 개발자" ], "summary": "Mediator가 access와 refresh token을 server-side authorized-client state에 보관하지만 access token만 브라우저 memory로 내보내고, 브라우저가 Resource Server용 Bearer header를 만든다.", "alt": "브라우저가 Spring mediator에서 access token만 받아 Resource Server를 직접 호출하고 refresh token은 authorized-client store에 남기는 AP2 split-custody 아키텍처.", "long_description": "브라우저는 AP2_SESSION으로 confidential Spring mediator의 login state를 사용한다. Mediator는 code를 교환한 뒤 access와 refresh token을 server-side authorized-client service에 저장한다. 현재 access token의 값, type, expiry만 브라우저로 전달되고 refresh token은 server 경계에 남는다. 브라우저는 memory에 있는 access token으로 Resource Server용 Bearer header를 만든다.", "source_context": { "document": "document.md", "document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371", "anchor": { "kind": "marker", "value": "ap2-mediator-architecture", "line": 162 } }, "composition": { "profile": "component-flow", "diagram_only": true, "reference_ids": [ "payment-event-flow" ], "rationale": "AP2의 핵심은 browser session에서 mediator state로 들어간 요청이 access-only 응답으로 browser에 돌아와 Resource Server 입력으로 이어지는 split-custody 경로다.", "focus_node": "mediator" }, "groups": [], "nodes": [ { "id": "browser", "label": "Browser", "kind": "actor", "role": "source", "shape": "actor", "description": "AP2_SESSION을 사용하고 전달받은 access token으로 API를 직접 호출하는 주체.", "details": [ "HttpOnly AP2_SESSION", "access token in memory" ], "evidence": [ { "start_line": 156, "end_line": 160 } ], "assumption": false }, { "id": "mediator", "label": "Spring mediator", "kind": "service", "role": "service", "emphasis": "primary", "description": "Confidential OAuth client로 code를 교환하고 browser에 access-only 응답을 제공한다.", "details": [ "oauth2Login", "confidential client" ], "evidence": [ { "start_line": 156, "end_line": 158 } ], "assumption": false }, { "id": "authorized-client-store", "label": "Authorized-client store", "kind": "database", "role": "store", "shape": "database", "description": "Access와 refresh token을 server-side state로 보관한다.", "details": [ "access token", "refresh token" ], "evidence": [ { "start_line": 156, "end_line": 160 } ], "assumption": false }, { "id": "api-resource-server", "label": "Resource Server", "kind": "service", "role": "sink", "description": "브라우저가 만든 Bearer header를 직접 받으며 downstream audience를 검증하는 API.", "evidence": [ { "start_line": 156, "end_line": 160 } ], "assumption": false } ], "edges": [ { "id": "browser-to-mediator", "from": "browser", "to": "mediator", "label": "AP2_SESSION", "kind": "request", "evidence": [ { "start_line": 158, "end_line": 160 } ], "assumption": false }, { "id": "mediator-to-store", "from": "mediator", "to": "authorized-client-store", "label": "보관 · 현재 access 조회", "kind": "data", "evidence": [ { "start_line": 156, "end_line": 160 } ], "assumption": false }, { "id": "mediator-to-browser", "from": "mediator", "to": "browser", "label": "access_token · type · expiry", "kind": "response", "style": "dashed", "emphasis": "primary", "evidence": [ { "start_line": 158, "end_line": 158 } ], "assumption": false }, { "id": "browser-to-api", "from": "browser", "to": "api-resource-server", "label": "Bearer header", "kind": "request", "emphasis": "primary", "evidence": [ { "start_line": 158, "end_line": 160 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "Mediator state와 browser API caller를 별도 node로 분리해 AP1·AP3와 다른 custody topology를 드러냈다." } }