{ "version": "1.1", "id": "ap2-mediator-handoff-flow", "title": "AP2 server-held token에서 브라우저 Bearer 요청까지", "question": "AP2에서 AP2_SESSION으로 찾은 server-side authorized client는 어떻게 브라우저의 직접 Bearer API 호출이 되는가?", "type": "sequence", "direction": "LR", "audience": [ "token mediator의 custody와 노출 경계를 검토하는 개발자" ], "summary": "Mediator는 refresh token을 server-side store에 남기면서 현재 access token만 JSON으로 브라우저에 반환하고, 브라우저가 그 값으로 Resource Server를 직접 호출한다.", "alt": "브라우저, Spring mediator, authorized-client store, Resource Server 사이에서 AP2_SESSION 요청, access-only 응답, 브라우저 Bearer 호출과 JSON 응답이 이어지는 순서도.", "long_description": "브라우저가 AP2_SESSION cookie와 함께 /token/access를 Spring mediator에 요청한다. Mediator는 현재 principal과 keycloak registration으로 authorized-client store에서 token을 조회하고 access token, type, expiry만 응답한다. 브라우저는 access token을 지역 변수로 받아 Authorization Bearer header를 만들고 Resource Server의 /api/me를 직접 호출한 뒤 사용자 JSON을 받는다. Refresh token은 브라우저 응답에 포함되지 않는다.", "source_context": { "document": "document.md", "document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371", "anchor": { "kind": "marker", "value": "ap2-mediator-handoff-flow", "line": 645 } }, "composition": { "profile": "sequence", "diagram_only": true, "reference_ids": [ "payment-approval-sequence" ], "rationale": "Session 요청, authorized-client 조회, access-only 응답, 브라우저 Bearer 호출과 API 응답의 순서가 명시되어 있어 ordered message가 서로 다른 왕복을 가장 명확히 분리한다.", "focus_node": "mediator" }, "groups": [], "nodes": [ { "id": "browser", "label": "브라우저", "kind": "participant", "role": "participant", "description": "AP2_SESSION으로 access endpoint를 호출하고 반환된 access token으로 Resource Server를 직접 호출한다.", "evidence": [ { "start_line": 505, "end_line": 511 }, { "start_line": 558, "end_line": 586 } ], "assumption": false }, { "id": "mediator", "label": "Spring mediator", "kind": "participant", "role": "participant", "emphasis": "primary", "description": "현재 principal로 authorized client를 조회하고 access token, type, expiry만 JSON으로 반환한다.", "evidence": [ { "start_line": 503, "end_line": 540 } ], "assumption": false }, { "id": "authorized-client-store", "label": "Authorized-client store", "kind": "participant", "role": "participant", "description": "Registration과 principal name으로 access token과 refresh token을 보관하는 process-local service.", "evidence": [ { "start_line": 445, "end_line": 457 }, { "start_line": 513, "end_line": 521 } ], "assumption": false }, { "id": "resource-server", "label": "Resource Server", "kind": "participant", "role": "participant", "description": "브라우저가 만든 Bearer JWT를 검증하고 /api/me 사용자 JSON을 반환한다.", "evidence": [ { "start_line": 571, "end_line": 599 } ], "assumption": false } ], "edges": [ { "id": "session-request", "from": "browser", "to": "mediator", "label": "GET /token/access + AP2_SESSION", "kind": "request", "order": 1, "evidence": [ { "start_line": 503, "end_line": 511 } ], "assumption": false }, { "id": "authorize-client", "from": "mediator", "to": "authorized-client-store", "label": "authorize current principal", "kind": "request", "order": 2, "evidence": [ { "start_line": 513, "end_line": 521 } ], "assumption": false }, { "id": "load-access-token", "from": "authorized-client-store", "to": "mediator", "label": "current access token", "kind": "response", "style": "dashed", "order": 3, "evidence": [ { "start_line": 513, "end_line": 521 } ], "assumption": false }, { "id": "access-only-response", "from": "mediator", "to": "browser", "label": "access token + type + expiry", "kind": "response", "style": "dashed", "order": 4, "evidence": [ { "start_line": 523, "end_line": 540 } ], "assumption": false }, { "id": "bearer-request", "from": "browser", "to": "resource-server", "label": "GET /api/me · browser-created Bearer", "kind": "request", "emphasis": "primary", "order": 5, "evidence": [ { "start_line": 558, "end_line": 588 } ], "assumption": false }, { "id": "identity-response", "from": "resource-server", "to": "browser", "label": "subject · username · issuer · audience", "kind": "response", "style": "dashed", "order": 6, "evidence": [ { "start_line": 590, "end_line": 615 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "Refresh token custody와 access token 전달을 혼동하지 않도록 happy-path access handoff의 메시지 순서만 표현했다." } }