refactor: 문서 개선 중
@@ -10,15 +10,15 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **브라우저 SPA** (participant): PKCE transaction과 token set을 처리하고 access token으로 API 요청을 만드는 public client. Evidence: L201–L236, L240–L280, L303–L322.
|
||||
- **Keycloak** (participant): Authorization code를 발급하고 code와 verifier를 token set으로 교환하는 authorization server. Evidence: L223–L263.
|
||||
- **Resource Server** (participant): Bearer JWT의 signature, issuer, timestamp와 audience를 검증하고 사용자 JSON을 만드는 Spring API. Evidence: L311–L353.
|
||||
- **브라우저 SPA** (participant): PKCE transaction과 token set을 처리하고 access token으로 API 요청을 만드는 public client. Evidence: L233–L268, L272–L312, L335–L354.
|
||||
- **Keycloak** (participant): Authorization code를 발급하고 code와 verifier를 token set으로 교환하는 authorization server. Evidence: L255–L295.
|
||||
- **Resource Server** (participant): Bearer JWT의 signature, issuer, timestamp와 audience를 검증하고 사용자 JSON을 만드는 Spring API. Evidence: L343–L385.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **브라우저 SPA → Keycloak:** authorization request · S256 challenge. Evidence: L223–L236.
|
||||
- **Keycloak → 브라우저 SPA:** callback · code + state. Evidence: L240–L250.
|
||||
- **브라우저 SPA → Keycloak:** token request · code + verifier. Evidence: L250–L261.
|
||||
- **Keycloak → 브라우저 SPA:** access + refresh + ID token. Evidence: L263–L280.
|
||||
- **브라우저 SPA → Resource Server:** GET /api/me · Bearer access token. Evidence: L303–L322.
|
||||
- **Resource Server → 브라우저 SPA:** subject · username · issuer · audience. Evidence: L342–L376.
|
||||
- **브라우저 SPA → Keycloak:** authorization request · S256 challenge. Evidence: L255–L268.
|
||||
- **Keycloak → 브라우저 SPA:** callback · code + state. Evidence: L272–L282.
|
||||
- **브라우저 SPA → Keycloak:** token request · code + verifier. Evidence: L282–L293.
|
||||
- **Keycloak → 브라우저 SPA:** access + refresh + ID token. Evidence: L295–L312.
|
||||
- **브라우저 SPA → Resource Server:** GET /api/me · Bearer access token. Evidence: L335–L354.
|
||||
- **Resource Server → 브라우저 SPA:** subject · username · issuer · audience. Evidence: L374–L408.
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser" value="브라우저 SPA" tooltip="PKCE transaction과 token set을 처리하고 access token으로 API 요청을 만드는 public client. | Evidence: L201-L236, L240-L280, L303-L322" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser" value="브라우저 SPA" tooltip="PKCE transaction과 token set을 처리하고 access token으로 API 요청을 만드는 public client. | Evidence: L233-L268, L272-L312, L335-L354" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_keycloak" value="Keycloak" tooltip="Authorization code를 발급하고 code와 verifier를 token set으로 교환하는 authorization server. | Evidence: L223-L263" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_keycloak" value="Keycloak" tooltip="Authorization code를 발급하고 code와 verifier를 token set으로 교환하는 authorization server. | Evidence: L255-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="Bearer JWT의 signature, issuer, timestamp와 audience를 검증하고 사용자 JSON을 만드는 Spring API. | Evidence: L311-L353" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="Bearer JWT의 signature, issuer, timestamp와 audience를 검증하고 사용자 JSON을 만드는 Spring API. | Evidence: L343-L385" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_authorize" value="authorization request · S256 challenge" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_keycloak">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap1-browser-bearer-flow",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "b78a7bf6ff5d1085c2063a500077d35ab4decf701d59c3d73dc05ca01512e936",
|
||||
"spec_sha256": "3da0d6ab32a63febbd0d6ef4c19deb5c091b70c3a79f9d6e8e8ac989fb8b9d71",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap1-browser-bearer-flow",
|
||||
"line": 395
|
||||
"line": 427
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap1-browser-bearer-flow.svg",
|
||||
"ap1-browser-bearer-flow.drawio",
|
||||
"ap1-browser-bearer-flow.mmd",
|
||||
"ap1-browser-bearer-flow.d2",
|
||||
"ap1-browser-bearer-flow.dot",
|
||||
"ap1-browser-bearer-flow.drawio",
|
||||
"ap1-browser-bearer-flow.excalidraw",
|
||||
"ap1-browser-bearer-flow.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="542" viewBox="0 0 680 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP1 callback code에서 브라우저 Bearer 요청까지</title>
|
||||
<desc id="diagram-description">브라우저 SPA가 S256 code challenge가 포함된 authorization request를 Keycloak에 보낸다. Keycloak이 code와 state를 callback으로 돌려주면 SPA는 원래 verifier를 포함해 token endpoint에 code를 제출하고 access, refresh, ID token을 받는다. 이어서 SPA가 access token을 Authorization Bearer header에 넣어 Resource Server의 /api/me를 직접 호출하고 사용자 JSON을 받는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap1-browser-bearer-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap1-browser-bearer-flow","line":395}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap1-browser-bearer-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"marker","value":"ap1-browser-bearer-flow","line":427}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,31 +50,31 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="201-236,240-280,303-322" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="233-268,272-312,335-354" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">브라우저 SPA</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="223-263" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="255-295" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Keycloak</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="311-353" x="465.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="343-385" x="465.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="540.0" y="65.0">Resource Server</text>
|
||||
<line class="lifeline" x1="540.0" y1="99.0" x2="540.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="223-236" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="255-268" />
|
||||
<rect class="edge-label-bg" x="78.7" y="114.0" width="292.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. authorization request · S256 challenge</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,202.0 120.0,202.0" data-evidence="240-250" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,202.0 120.0,202.0" data-evidence="272-282" />
|
||||
<rect class="edge-label-bg" x="128.9" y="176.0" width="192.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="191.0">2. callback · code + state</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,264.0 330.0,264.0" data-evidence="250-261" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,264.0 330.0,264.0" data-evidence="282-293" />
|
||||
<rect class="edge-label-bg" x="102.1" y="238.0" width="245.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="253.0">3. token request · code + verifier</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="263-280" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="295-312" />
|
||||
<rect class="edge-label-bg" x="115.5" y="300.0" width="219.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="315.0">4. access + refresh + ID token</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="120.0,388.0 540.0,388.0" data-evidence="303-322" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="120.0,388.0 540.0,388.0" data-evidence="335-354" />
|
||||
<rect class="edge-label-bg" x="200.4" y="362.0" width="259.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="330.0" y="377.0">5. GET /api/me · Bearer access token</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="540.0,450.0 120.0,450.0" data-evidence="342-376" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="540.0,450.0 120.0,450.0" data-evidence="374-408" />
|
||||
<rect class="edge-label-bg" x="183.7" y="424.0" width="292.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="330.0" y="439.0">6. subject · username · issuer · audience</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
@@ -10,14 +10,14 @@ SPA, Keycloak, 브라우저 JavaScript memory, Resource Server가 왼쪽에서
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **Public SPA** (service): Authorization Code와 PKCE를 직접 다루고 access JWT로 Resource Server를 호출하는 public OAuth client. Evidence: L144–L148.
|
||||
- **Browser JS memory** (database): Access, refresh, ID token을 실행 중 보관하며 reload 뒤에는 복구하지 않는 browser-side custody. Evidence: L148–L150.
|
||||
- **Keycloak** (service): SPA의 code 교환 상대이며 API가 검증하는 JWT 서명의 출처. Evidence: L144–L146.
|
||||
- **Resource Server** (service): Access JWT의 issuer, 시간 제약과 audience를 검증하는 API. Evidence: L144–L150.
|
||||
- **Public SPA** (service): Authorization Code와 PKCE를 직접 다루고 access JWT로 Resource Server를 호출하는 public OAuth client. Evidence: L168–L172.
|
||||
- **Browser JS memory** (database): Access, refresh, ID token을 실행 중 보관하며 reload 뒤에는 복구하지 않는 browser-side custody. Evidence: L172–L174.
|
||||
- **Keycloak** (service): SPA의 code 교환 상대이며 API가 검증하는 JWT 서명의 출처. Evidence: L168–L170.
|
||||
- **Resource Server** (service): Access JWT의 issuer, 시간 제약과 audience를 검증하는 API. Evidence: L168–L174.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Keycloak → Public SPA:** access · refresh · ID token. Evidence: L144–L148.
|
||||
- **Public SPA → Keycloak:** Authorization Code + PKCE S256. Evidence: L144–L146.
|
||||
- **Public SPA → Browser JS memory:** token set 보관. Evidence: L148–L150.
|
||||
- **Public SPA → Resource Server:** memory-held access JWT · Bearer. Evidence: L144–L150.
|
||||
- **Keycloak → Public SPA:** access · refresh · ID token. Evidence: L168–L172.
|
||||
- **Public SPA → Keycloak:** Authorization Code + PKCE S256. Evidence: L168–L170.
|
||||
- **Public SPA → Browser JS memory:** token set 보관. Evidence: L172–L174.
|
||||
- **Public SPA → Resource Server:** memory-held access JWT · Bearer. Evidence: L168–L174.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser-spa" value="Public SPA<br/>spa-public<br/>PKCE S256" tooltip="Authorization Code와 PKCE를 직접 다루고 access JWT로 Resource Server를 호출하는 public OAuth client. | Evidence: L144-L148" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser-spa" value="Public SPA<br/>spa-public<br/>PKCE S256" tooltip="Authorization Code와 PKCE를 직접 다루고 access JWT로 Resource Server를 호출하는 public OAuth client. | Evidence: L168-L172" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="208.0" width="150.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_api-resource-server" value="Resource Server<br/>issuer · time<br/>keycloak-pattern-api audience" tooltip="Access JWT의 issuer, 시간 제약과 audience를 검증하는 API. | Evidence: L144-L150" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_api-resource-server" value="Resource Server<br/>issuer · time<br/>keycloak-pattern-api audience" tooltip="Access JWT의 issuer, 시간 제약과 audience를 검증하는 API. | Evidence: L168-L174" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="60.0" width="237.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_browser-token-memory" value="Browser JS memory<br/>access · refresh · ID token<br/>persistent Web Storage 없음" tooltip="Access, refresh, ID token을 실행 중 보관하며 reload 뒤에는 복구하지 않는 browser-side custody. | Evidence: L148-L150" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser-token-memory" value="Browser JS memory<br/>access · refresh · ID token<br/>persistent Web Storage 없음" tooltip="Access, refresh, ID token을 실행 중 보관하며 reload 뒤에는 복구하지 않는 browser-side custody. | Evidence: L172-L174" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="387.0" y="220.0" width="223.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_keycloak" value="Keycloak" tooltip="SPA의 code 교환 상대이며 API가 검증하는 JWT 서명의 출처. | Evidence: L144-L146" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_keycloak" value="Keycloak" tooltip="SPA의 code 교환 상대이며 API가 검증하는 JWT 서명의 출처. | Evidence: L168-L170" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="423.5" y="380.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_keycloak-to-spa" value="access · refresh · ID token" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak" target="n_browser-spa">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap1-direct-architecture",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "ea8f3f861258a68eabe2e902fb8a5b85ce250a68ef5893eefa8701f1b8eb3627",
|
||||
"spec_sha256": "b4cf9b9f65e7caca89b4efea39dbab413c71228f327acb8168e3f053e8bfe9c9",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap1-direct-architecture",
|
||||
"line": 152
|
||||
"line": 176
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap1-direct-architecture.svg",
|
||||
"ap1-direct-architecture.drawio",
|
||||
"ap1-direct-architecture.mmd",
|
||||
"ap1-direct-architecture.d2",
|
||||
"ap1-direct-architecture.dot",
|
||||
"ap1-direct-architecture.drawio",
|
||||
"ap1-direct-architecture.excalidraw",
|
||||
"ap1-direct-architecture.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="535" viewBox="0 0 680 535" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP1은 OAuth client와 token custody를 브라우저에 둔다</title>
|
||||
<desc id="diagram-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를 검증한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap1-direct-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap1-direct-architecture","line":152}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap1-direct-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9","anchor":{"kind":"marker","value":"ap1-direct-architecture","line":176}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,41 +50,41 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="535" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="498.5,444.0 498.5,490.0 145.0,490.0 145.0,296.0" data-evidence="144-148" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="498.5,444.0 498.5,490.0 145.0,490.0 145.0,296.0" data-evidence="168-172" />
|
||||
<rect class="edge-label-bg" x="148.3" y="448.0" width="198.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="247.8" y="463.0">access · refresh · ID token</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="220.0,279.0 331.8,279.0 331.8,421.0 423.5,421.0" data-evidence="144-146" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="220.0,279.0 331.8,279.0 331.8,421.0 423.5,421.0" data-evidence="168-170" />
|
||||
<rect class="edge-label-bg" x="198.2" y="326.0" width="219.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="307.8" y="341.0">Authorization Code + PKCE S256</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,243.0 303.5,243.0 303.5,264.0 387.0,264.0" data-evidence="148-150" />
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,243.0 303.5,243.0 303.5,264.0 387.0,264.0" data-evidence="172-174" />
|
||||
<rect class="edge-label-bg" x="278.3" y="239.5" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="327.5" y="254.5">token set 보관</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="220.0,225.0 300.0,225.0 300.0,104.0 380.0,104.0" data-evidence="144-150" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="220.0,225.0 300.0,225.0 300.0,104.0 380.0,104.0" data-evidence="168-174" />
|
||||
<rect class="edge-label-bg" x="211.1" y="150.5" width="225.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="165.5">memory-held access JWT · Bearer</text>
|
||||
<g id="node-browser-spa">
|
||||
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="144-148" x="70.0" y="208.0" width="150.0" height="88.0" rx="7" />
|
||||
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="168-172" x="70.0" y="208.0" width="150.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="235.0">Public SPA</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="256.0" x2="206.0" y2="256.0" />
|
||||
<text class="node-detail" x="86.0" y="273.0">spa-public</text>
|
||||
<text class="node-detail" x="86.0" y="289.0">PKCE S256</text>
|
||||
</g>
|
||||
<g id="node-api-resource-server">
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="144-150" x="380.0" y="60.0" width="237.0" height="88.0" rx="7" />
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="168-174" x="380.0" y="60.0" width="237.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="498.5" y="87.0">Resource Server</text>
|
||||
<line class="node-detail-divider" x1="394.0" y1="108.0" x2="603.0" y2="108.0" />
|
||||
<text class="node-detail" x="396.0" y="125.0">issuer · time</text>
|
||||
<text class="node-detail" x="396.0" y="141.0">keycloak-pattern-api audience</text>
|
||||
</g>
|
||||
<g id="node-browser-token-memory">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="148-150" x="387.0" y="220.0" width="223.0" height="88.0" rx="7" />
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="172-174" x="387.0" y="220.0" width="223.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="498.5" y="247.0">Browser JS memory</text>
|
||||
<line class="node-detail-divider" x1="401.0" y1="268.0" x2="596.0" y2="268.0" />
|
||||
<text class="node-detail" x="403.0" y="285.0">access · refresh · ID token</text>
|
||||
<text class="node-detail" x="403.0" y="301.0">persistent Web Storage 없음</text>
|
||||
</g>
|
||||
<g id="node-keycloak">
|
||||
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="144-146" x="423.5" y="380.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="168-170" x="423.5" y="380.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="498.5" y="410.0">Keycloak</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -10,14 +10,14 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **Browser** (actor): AP2_SESSION을 사용하고 전달받은 access token으로 API를 직접 호출하는 주체. Evidence: L156–L160.
|
||||
- **Spring mediator** (service): Confidential OAuth client로 code를 교환하고 browser에 access-only 응답을 제공한다. Evidence: L156–L158.
|
||||
- **Authorized-client store** (database): Access와 refresh token을 server-side state로 보관한다. Evidence: L156–L160.
|
||||
- **Resource Server** (service): 브라우저가 만든 Bearer header를 직접 받으며 downstream audience를 검증하는 API. Evidence: L156–L160.
|
||||
- **Browser** (actor): AP2_SESSION을 사용하고 전달받은 access token으로 API를 직접 호출하는 주체. Evidence: L182–L186.
|
||||
- **Spring mediator** (service): Confidential OAuth client로 code를 교환하고 browser에 access-only 응답을 제공한다. Evidence: L182–L184.
|
||||
- **Authorized-client store** (database): Access와 refresh token을 server-side state로 보관한다. Evidence: L182–L186.
|
||||
- **Resource Server** (service): 브라우저가 만든 Bearer header를 직접 받으며 downstream audience를 검증하는 API. Evidence: L182–L186.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Browser → Resource Server:** Bearer header. Evidence: L158–L160.
|
||||
- **Browser → Spring mediator:** AP2_SESSION. Evidence: L158–L160.
|
||||
- **Spring mediator → Browser:** access_token · type · expiry. Evidence: L158–L158.
|
||||
- **Spring mediator → Authorized-client store:** 보관 · 조회. Evidence: L156–L160.
|
||||
- **Browser → Resource Server:** Bearer header. Evidence: L184–L186.
|
||||
- **Browser → Spring mediator:** AP2_SESSION. Evidence: L184–L186.
|
||||
- **Spring mediator → Browser:** access_token · type · expiry. Evidence: L184–L184.
|
||||
- **Spring mediator → Authorized-client store:** 보관 · 조회. Evidence: L182–L186.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser" value="Browser<br/>HttpOnly AP2_SESSION<br/>access token in memory" tooltip="AP2_SESSION을 사용하고 전달받은 access token으로 API를 직접 호출하는 주체. | Evidence: L156-L160" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser" value="Browser<br/>HttpOnly AP2_SESSION<br/>access token in memory" tooltip="AP2_SESSION을 사용하고 전달받은 access token으로 API를 직접 호출하는 주체. | Evidence: L182-L186" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="128.0" width="170.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_api-resource-server" value="Resource Server" tooltip="브라우저가 만든 Bearer header를 직접 받으며 downstream audience를 검증하는 API. | Evidence: L156-L160" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_api-resource-server" value="Resource Server" tooltip="브라우저가 만든 Bearer header를 직접 받으며 downstream audience를 검증하는 API. | Evidence: L182-L186" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="408.5" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_mediator" value="Spring mediator<br/>oauth2Login<br/>confidential client" tooltip="Confidential OAuth client로 code를 교환하고 browser에 access-only 응답을 제공한다. | Evidence: L156-L158" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_mediator" value="Spring mediator<br/>oauth2Login<br/>confidential client" tooltip="Confidential OAuth client로 code를 교환하고 browser에 access-only 응답을 제공한다. | Evidence: L182-L184" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="400.0" y="196.0" width="167.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store<br/>access token<br/>refresh token" tooltip="Access와 refresh token을 server-side state로 보관한다. | Evidence: L156-L160" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store<br/>access token<br/>refresh token" tooltip="Access와 refresh token을 server-side state로 보관한다. | Evidence: L182-L186" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="727.0" y="118.0" width="153.0" height="108.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_browser-to-api" value="Bearer header" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_api-resource-server">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap2-mediator-architecture",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "57354aea377e1c3f11e19fd03ccffba9df44f37b78a5fa69f8b2521705f8f6d7",
|
||||
"spec_sha256": "f2bd21d57d7feb8318d8b1c4f06c4cf708b2e8ca7be38b1ae36b851c62a280c1",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap2-mediator-architecture",
|
||||
"line": 162
|
||||
"line": 188
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap2-mediator-architecture.svg",
|
||||
"ap2-mediator-architecture.drawio",
|
||||
"ap2-mediator-architecture.mmd",
|
||||
"ap2-mediator-architecture.d2",
|
||||
"ap2-mediator-architecture.dot",
|
||||
"ap2-mediator-architecture.drawio",
|
||||
"ap2-mediator-architecture.excalidraw",
|
||||
"ap2-mediator-architecture.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="925" height="393" viewBox="0 0 925 393" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP2는 refresh custody와 API caller를 서로 다른 경계에 둔다</title>
|
||||
<desc id="diagram-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를 만든다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap2-mediator-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap2-mediator-architecture","line":162}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap2-mediator-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"marker","value":"ap2-mediator-architecture","line":188}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,35 +50,35 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="925" height="393" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="240.0,154.0 324.2,154.0 324.2,92.0 408.5,92.0" data-evidence="158-160" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="240.0,154.0 324.2,154.0 324.2,92.0 408.5,92.0" data-evidence="184-186" />
|
||||
<rect class="edge-label-bg" x="295.7" y="109.0" width="105.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="348.2" y="124.0">Bearer header</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="240.0,172.0 320.0,172.0 320.0,231.0 400.0,231.0" data-evidence="158-160" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="240.0,172.0 320.0,172.0 320.0,231.0 400.0,231.0" data-evidence="184-186" />
|
||||
<rect class="edge-label-bg" x="298.1" y="187.5" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="344.0" y="202.5">AP2_SESSION</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-primary semantic-dashed" points="495.5,284.0 495.5,348.0 167.0,348.0 167.0,216.0" data-evidence="158-158" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-primary semantic-dashed" points="495.5,284.0 495.5,348.0 167.0,348.0 167.0,216.0" data-evidence="184-184" />
|
||||
<rect class="edge-label-bg" x="194.4" y="362.0" width="205.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="297.2" y="377.0">access_token · type · expiry</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="567.0,240.0 647.0,240.0 647.0,172.0 727.0,172.0" data-evidence="156-160" />
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="567.0,240.0 647.0,240.0 647.0,172.0 727.0,172.0" data-evidence="182-186" />
|
||||
<rect class="edge-label-bg" x="638.5" y="192.0" width="64.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="671.0" y="207.0">보관 · 조회</text>
|
||||
<g id="node-browser">
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="156-160"><circle cx="155.0" cy="148.0" r="11.0" /><line x1="155.0" y1="164.0" x2="155.0" y2="177.0" /><line x1="137.0" y1="174.0" x2="173.0" y2="174.0" /><line x1="155.0" y1="177.0" x2="140.0" y2="194.0" /><line x1="155.0" y1="177.0" x2="170.0" y2="194.0" /></g>
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="182-186"><circle cx="155.0" cy="148.0" r="11.0" /><line x1="155.0" y1="164.0" x2="155.0" y2="187.0" /><line x1="137.0" y1="174.0" x2="173.0" y2="174.0" /><line x1="155.0" y1="187.0" x2="140.0" y2="204.0" /><line x1="155.0" y1="187.0" x2="170.0" y2="204.0" /></g>
|
||||
<text class="node-label" x="155.0" y="209.0">Browser</text>
|
||||
</g>
|
||||
<g id="node-api-resource-server">
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="156-160" x="408.5" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="182-186" x="408.5" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="483.5" y="90.0">Resource Server</text>
|
||||
</g>
|
||||
<g id="node-mediator">
|
||||
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="156-158" x="400.0" y="196.0" width="167.0" height="88.0" rx="7" />
|
||||
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="182-184" x="400.0" y="196.0" width="167.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="483.5" y="223.0">Spring mediator</text>
|
||||
<line class="node-detail-divider" x1="414.0" y1="244.0" x2="553.0" y2="244.0" />
|
||||
<text class="node-detail" x="416.0" y="261.0">oauth2Login</text>
|
||||
<text class="node-detail" x="416.0" y="277.0">confidential client</text>
|
||||
</g>
|
||||
<g id="node-authorized-client-store">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="156-160" x="727.0" y="118.0" width="153.0" height="108.0" rx="7" />
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="182-186" x="727.0" y="118.0" width="153.0" height="108.0" rx="7" />
|
||||
<text class="node-label" x="803.5" y="145.0">Authorized-client</text>
|
||||
<text class="node-label" x="803.5" y="163.0">store</text>
|
||||
<line class="node-detail-divider" x1="741.0" y1="184.0" x2="866.0" y2="184.0" />
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
@@ -10,16 +10,16 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **브라우저** (participant): AP2_SESSION으로 access endpoint를 호출하고 반환된 access token으로 Resource Server를 직접 호출한다. Evidence: L505–L511, L558–L586.
|
||||
- **Spring mediator** (participant): 현재 principal로 authorized client를 조회하고 access token, type, expiry만 JSON으로 반환한다. Evidence: L503–L540.
|
||||
- **Authorized-client store** (participant): Registration과 principal name으로 access token과 refresh token을 보관하는 process-local service. Evidence: L445–L457, L513–L521.
|
||||
- **Resource Server** (participant): 브라우저가 만든 Bearer JWT를 검증하고 /api/me 사용자 JSON을 반환한다. Evidence: L571–L599.
|
||||
- **브라우저** (participant): AP2_SESSION으로 access endpoint를 호출하고 반환된 access token으로 Resource Server를 직접 호출한다. Evidence: L539–L545, L592–L620.
|
||||
- **Spring mediator** (participant): 현재 principal로 authorized client를 조회하고 access token, type, expiry만 JSON으로 반환한다. Evidence: L537–L574.
|
||||
- **Authorized-client store** (participant): Registration과 principal name으로 access token과 refresh token을 보관하는 process-local service. Evidence: L479–L491, L547–L555.
|
||||
- **Resource Server** (participant): 브라우저가 만든 Bearer JWT를 검증하고 /api/me 사용자 JSON을 반환한다. Evidence: L605–L633.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **브라우저 → Spring mediator:** GET /token/access + AP2_SESSION. Evidence: L503–L511.
|
||||
- **Spring mediator → Authorized-client store:** authorize current principal. Evidence: L513–L521.
|
||||
- **Authorized-client store → Spring mediator:** current access token. Evidence: L513–L521.
|
||||
- **Spring mediator → 브라우저:** access token + type + expiry. Evidence: L523–L540.
|
||||
- **브라우저 → Resource Server:** GET /api/me · browser-created Bearer. Evidence: L558–L588.
|
||||
- **Resource Server → 브라우저:** subject · username · issuer · audience. Evidence: L590–L615.
|
||||
- **브라우저 → Spring mediator:** GET /token/access + AP2_SESSION. Evidence: L537–L545.
|
||||
- **Spring mediator → Authorized-client store:** authorize current principal. Evidence: L547–L555.
|
||||
- **Authorized-client store → Spring mediator:** current access token. Evidence: L547–L555.
|
||||
- **Spring mediator → 브라우저:** access token + type + expiry. Evidence: L557–L574.
|
||||
- **브라우저 → Resource Server:** GET /api/me · browser-created Bearer. Evidence: L592–L622.
|
||||
- **Resource Server → 브라우저:** subject · username · issuer · audience. Evidence: L624–L649.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="AP2_SESSION으로 access endpoint를 호출하고 반환된 access token으로 Resource Server를 직접 호출한다. | Evidence: L505-L511, L558-L586" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="AP2_SESSION으로 access endpoint를 호출하고 반환된 access token으로 Resource Server를 직접 호출한다. | Evidence: L539-L545, L592-L620" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_mediator" value="Spring mediator" tooltip="현재 principal로 authorized client를 조회하고 access token, type, expiry만 JSON으로 반환한다. | Evidence: L503-L540" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_mediator" value="Spring mediator" tooltip="현재 principal로 authorized client를 조회하고 access token, type, expiry만 JSON으로 반환한다. | Evidence: L537-L574" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store" tooltip="Registration과 principal name으로 access token과 refresh token을 보관하는 process-local service. | Evidence: L445-L457, L513-L521" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store" tooltip="Registration과 principal name으로 access token과 refresh token을 보관하는 process-local service. | Evidence: L479-L491, L547-L555" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="153.0" height="74.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="브라우저가 만든 Bearer JWT를 검증하고 /api/me 사용자 JSON을 반환한다. | Evidence: L571-L599" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="브라우저가 만든 Bearer JWT를 검증하고 /api/me 사용자 JSON을 반환한다. | Evidence: L605-L633" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="675.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_session-request" value="GET /token/access + AP2_SESSION" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_mediator">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap2-mediator-handoff-flow",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "b129ffe1dee64fe9ee2f29d59ad47472ae9fe9511c92f3199fa053ba0c3b2c3f",
|
||||
"spec_sha256": "01ceea7a8441554faea1b520c5af568878d6dbcc091fda3aae8a2dcd15e2a48d",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap2-mediator-handoff-flow",
|
||||
"line": 645
|
||||
"line": 679
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap2-mediator-handoff-flow.svg",
|
||||
"ap2-mediator-handoff-flow.drawio",
|
||||
"ap2-mediator-handoff-flow.mmd",
|
||||
"ap2-mediator-handoff-flow.d2",
|
||||
"ap2-mediator-handoff-flow.dot",
|
||||
"ap2-mediator-handoff-flow.drawio",
|
||||
"ap2-mediator-handoff-flow.excalidraw",
|
||||
"ap2-mediator-handoff-flow.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="870" height="542" viewBox="0 0 870 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP2 server-held token에서 브라우저 Bearer 요청까지</title>
|
||||
<desc id="diagram-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은 브라우저 응답에 포함되지 않는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap2-mediator-handoff-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap2-mediator-handoff-flow","line":645}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap2-mediator-handoff-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"marker","value":"ap2-mediator-handoff-flow","line":679}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,35 +50,35 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="870" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="505-511,558-586" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="539-545,592-620" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">브라우저</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="503-540" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="537-574" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Spring mediator</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="445-457,513-521" x="465.0" y="35.0" width="153.0" height="74.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="479-491,547-555" x="465.0" y="35.0" width="153.0" height="74.0" rx="7" />
|
||||
<text class="node-label" x="541.5" y="62.0">Authorized-client</text>
|
||||
<text class="node-label" x="541.5" y="80.0">store</text>
|
||||
<line class="lifeline" x1="541.5" y1="109.0" x2="541.5" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="571-599" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="605-633" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="750.0" y="65.0">Resource Server</text>
|
||||
<line class="lifeline" x1="750.0" y1="99.0" x2="750.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="503-511" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="537-545" />
|
||||
<rect class="edge-label-bg" x="102.1" y="114.0" width="245.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. GET /token/access + AP2_SESSION</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 541.5,202.0" data-evidence="513-521" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 541.5,202.0" data-evidence="547-555" />
|
||||
<rect class="edge-label-bg" x="326.2" y="176.0" width="219.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.8" y="191.0">2. authorize current principal</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="541.5,264.0 330.0,264.0" data-evidence="513-521" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="541.5,264.0 330.0,264.0" data-evidence="547-555" />
|
||||
<rect class="edge-label-bg" x="349.7" y="238.0" width="172.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.8" y="253.0">3. current access token</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="523-540" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="557-574" />
|
||||
<rect class="edge-label-bg" x="112.1" y="300.0" width="225.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="315.0">4. access token + type + expiry</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="120.0,388.0 750.0,388.0" data-evidence="558-588" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="120.0,388.0 750.0,388.0" data-evidence="592-622" />
|
||||
<rect class="edge-label-bg" x="295.4" y="362.0" width="279.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="377.0">5. GET /api/me · browser-created Bearer</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,450.0 120.0,450.0" data-evidence="590-615" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,450.0 120.0,450.0" data-evidence="624-649" />
|
||||
<rect class="edge-label-bg" x="288.6" y="424.0" width="292.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="439.0">6. subject · username · issuer · audience</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -2,10 +2,10 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap3-bff-architecture",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "ea691460c8489d465100ce6d75ca39b6d6283b51b5144fc9383f98d455b6b261",
|
||||
"spec_sha256": "5bd592ebe127bb911720fd5cc6809e2ba3cff8137283bdcffe4b480efa1ba262",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "AP3: browser token 비노출과 application-owned session을 맞바꾼다",
|
||||
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"outputs": [
|
||||
"ap3-bff-architecture.svg",
|
||||
"ap3-bff-architecture.drawio",
|
||||
"ap3-bff-architecture.mmd",
|
||||
"ap3-bff-architecture.d2",
|
||||
"ap3-bff-architecture.dot",
|
||||
"ap3-bff-architecture.drawio",
|
||||
"ap3-bff-architecture.excalidraw",
|
||||
"ap3-bff-architecture.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="938" height="406" viewBox="0 0 938 406" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP3는 browser session을 BFF-owned Bearer 요청으로 바꾼다</title>
|
||||
<desc id="diagram-description">왼쪽 browser session zone에는 OAuth token 없이 HttpOnly AP3_SESSION을 가진 브라우저가 있다. 오른쪽 server-side application zone에는 BFF, authorized-client store, 내부 Resource Server가 있다. 브라우저의 /bff/api/me 요청은 BFF에서 종료되고, BFF는 current authorized client를 조회해 server-held access token으로 Resource Server용 Bearer 요청을 조립한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-bff-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37","anchor":{"kind":"heading","value":"AP3: browser token 비노출과 application-owned session을 맞바꾼다","line":190}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-bff-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"heading","value":"AP3: browser token 비노출과 application-owned session을 맞바꾼다","line":190}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -66,7 +66,7 @@
|
||||
<rect class="edge-label-bg" x="274.1" y="166.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="320.0" y="181.0">AP3_SESSION</text>
|
||||
<g id="node-browser">
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="192-194"><circle cx="155.0" cy="184.0" r="11.0" /><line x1="155.0" y1="200.0" x2="155.0" y2="213.0" /><line x1="137.0" y1="210.0" x2="173.0" y2="210.0" /><line x1="155.0" y1="213.0" x2="140.0" y2="230.0" /><line x1="155.0" y1="213.0" x2="170.0" y2="230.0" /></g>
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="192-194"><circle cx="155.0" cy="184.0" r="11.0" /><line x1="155.0" y1="200.0" x2="155.0" y2="223.0" /><line x1="137.0" y1="210.0" x2="173.0" y2="210.0" /><line x1="155.0" y1="223.0" x2="140.0" y2="240.0" /><line x1="155.0" y1="223.0" x2="170.0" y2="240.0" /></g>
|
||||
<text class="node-label" x="155.0" y="245.0">Browser</text>
|
||||
</g>
|
||||
<g id="node-bff">
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -10,16 +10,16 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **브라우저** (participant): Authorization header 없이 AP3_SESSION cookie로 BFF endpoint를 호출하고 중계 JSON을 받는다. Evidence: L714–L722, L750–L760, L796–L809.
|
||||
- **Spring BFF** (participant): Session authentication을 authorized client 조회로 바꾸고 Resource Server용 Bearer 요청을 조립한다. Evidence: L750–L783, L796–L809.
|
||||
- **Authorized-client store** (participant): Registration과 principal name으로 BFF의 access token과 refresh token을 보관한다. Evidence: L700–L712, L762–L768.
|
||||
- **Resource Server** (participant): BFF가 붙인 Bearer JWT를 검증하고 사용자 JSON을 반환한다. Evidence: L776–L796.
|
||||
- **브라우저** (participant): Authorization header 없이 AP3_SESSION cookie로 BFF endpoint를 호출하고 중계 JSON을 받는다. Evidence: L750–L758, L786–L796, L832–L845.
|
||||
- **Spring BFF** (participant): Session authentication을 authorized client 조회로 바꾸고 Resource Server용 Bearer 요청을 조립한다. Evidence: L786–L819, L832–L845.
|
||||
- **Authorized-client store** (participant): Registration과 principal name으로 BFF의 access token과 refresh token을 보관한다. Evidence: L736–L748, L798–L804.
|
||||
- **Resource Server** (participant): BFF가 붙인 Bearer JWT를 검증하고 사용자 JSON을 반환한다. Evidence: L812–L832.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **브라우저 → Spring BFF:** GET /bff/api/me + AP3_SESSION. Evidence: L750–L760.
|
||||
- **Spring BFF → Authorized-client store:** authorize current principal. Evidence: L762–L768.
|
||||
- **Authorized-client store → Spring BFF:** server-held access token. Evidence: L762–L768.
|
||||
- **Spring BFF → Resource Server:** GET /api/me · Bearer access token. Evidence: L770–L783.
|
||||
- **Resource Server → Spring BFF:** subject · username · issuer · audience. Evidence: L785–L796.
|
||||
- **Spring BFF → 브라우저:** BFF ResponseEntity → browser JSON. Evidence: L796–L809.
|
||||
- **브라우저 → Spring BFF:** GET /bff/api/me + AP3_SESSION. Evidence: L786–L796.
|
||||
- **Spring BFF → Authorized-client store:** authorize current principal. Evidence: L798–L804.
|
||||
- **Authorized-client store → Spring BFF:** server-held access token. Evidence: L798–L804.
|
||||
- **Spring BFF → Resource Server:** GET /api/me · Bearer access token. Evidence: L806–L819.
|
||||
- **Resource Server → Spring BFF:** subject · username · issuer · audience. Evidence: L821–L832.
|
||||
- **Spring BFF → 브라우저:** BFF ResponseEntity → browser JSON. Evidence: L832–L845.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="Authorization header 없이 AP3_SESSION cookie로 BFF endpoint를 호출하고 중계 JSON을 받는다. | Evidence: L714-L722, L750-L760, L796-L809" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="Authorization header 없이 AP3_SESSION cookie로 BFF endpoint를 호출하고 중계 JSON을 받는다. | Evidence: L750-L758, L786-L796, L832-L845" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_bff" value="Spring BFF" tooltip="Session authentication을 authorized client 조회로 바꾸고 Resource Server용 Bearer 요청을 조립한다. | Evidence: L750-L783, L796-L809" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_bff" value="Spring BFF" tooltip="Session authentication을 authorized client 조회로 바꾸고 Resource Server용 Bearer 요청을 조립한다. | Evidence: L786-L819, L832-L845" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store" tooltip="Registration과 principal name으로 BFF의 access token과 refresh token을 보관한다. | Evidence: L700-L712, L762-L768" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_authorized-client-store" value="Authorized-client store" tooltip="Registration과 principal name으로 BFF의 access token과 refresh token을 보관한다. | Evidence: L736-L748, L798-L804" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="153.0" height="74.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="BFF가 붙인 Bearer JWT를 검증하고 사용자 JSON을 반환한다. | Evidence: L776-L796" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_resource-server" value="Resource Server" tooltip="BFF가 붙인 Bearer JWT를 검증하고 사용자 JSON을 반환한다. | Evidence: L812-L832" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="675.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_session-request" value="GET /bff/api/me + AP3_SESSION" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_bff">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap3-bff-session-flow",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "2030434c650208507b9ad33f894d4de71990b1b550fa4652cddf4a6c50141d2b",
|
||||
"spec_sha256": "f7b05b118b25590243f09fb312fd40794dba7bc0a7f76d09d74c8d3ff1484a81",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap3-bff-session-flow",
|
||||
"line": 908
|
||||
"line": 944
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap3-bff-session-flow.svg",
|
||||
"ap3-bff-session-flow.drawio",
|
||||
"ap3-bff-session-flow.mmd",
|
||||
"ap3-bff-session-flow.d2",
|
||||
"ap3-bff-session-flow.dot",
|
||||
"ap3-bff-session-flow.drawio",
|
||||
"ap3-bff-session-flow.excalidraw",
|
||||
"ap3-bff-session-flow.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="870" height="542" viewBox="0 0 870 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP3 session cookie에서 BFF downstream Bearer까지</title>
|
||||
<desc id="diagram-description">브라우저가 Authorization header 없이 AP3_SESSION cookie로 /bff/api/me를 호출한다. BFF는 현재 Authentication으로 authorized-client manager를 호출해 server-held access token을 얻고 Resource Server의 /api/me에 Bearer header를 붙인다. Resource Server가 JWT를 검증해 사용자 JSON을 반환하면 BFF가 ResponseEntity로 받아 브라우저에 중계한다. 브라우저 session cookie는 downstream으로 전달되지 않는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-bff-session-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap3-bff-session-flow","line":908}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-bff-session-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"marker","value":"ap3-bff-session-flow","line":944}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,35 +50,35 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="870" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="714-722,750-760,796-809" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="750-758,786-796,832-845" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">브라우저</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="750-783,796-809" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="786-819,832-845" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Spring BFF</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="700-712,762-768" x="465.0" y="35.0" width="153.0" height="74.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="736-748,798-804" x="465.0" y="35.0" width="153.0" height="74.0" rx="7" />
|
||||
<text class="node-label" x="541.5" y="62.0">Authorized-client</text>
|
||||
<text class="node-label" x="541.5" y="80.0">store</text>
|
||||
<line class="lifeline" x1="541.5" y1="109.0" x2="541.5" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="776-796" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="812-832" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="750.0" y="65.0">Resource Server</text>
|
||||
<line class="lifeline" x1="750.0" y1="99.0" x2="750.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="750-760" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="786-796" />
|
||||
<rect class="edge-label-bg" x="108.8" y="114.0" width="232.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. GET /bff/api/me + AP3_SESSION</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 541.5,202.0" data-evidence="762-768" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 541.5,202.0" data-evidence="798-804" />
|
||||
<rect class="edge-label-bg" x="326.2" y="176.0" width="219.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.8" y="191.0">2. authorize current principal</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="541.5,264.0 330.0,264.0" data-evidence="762-768" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="541.5,264.0 330.0,264.0" data-evidence="798-804" />
|
||||
<rect class="edge-label-bg" x="336.3" y="238.0" width="198.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.8" y="253.0">3. server-held access token</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="330.0,326.0 750.0,326.0" data-evidence="770-783" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="330.0,326.0 750.0,326.0" data-evidence="806-819" />
|
||||
<rect class="edge-label-bg" x="410.4" y="300.0" width="259.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="540.0" y="315.0">4. GET /api/me · Bearer access token</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,388.0 330.0,388.0" data-evidence="785-796" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,388.0 330.0,388.0" data-evidence="821-832" />
|
||||
<rect class="edge-label-bg" x="393.6" y="362.0" width="292.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="540.0" y="377.0">5. subject · username · issuer · audience</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,450.0 120.0,450.0" data-evidence="796-809" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,450.0 120.0,450.0" data-evidence="832-845" />
|
||||
<rect class="edge-label-bg" x="95.4" y="424.0" width="259.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="439.0">6. BFF ResponseEntity → browser JSON</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -2,10 +2,10 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap3-csrf-boundary",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "d10a5d627e8436b300e3bb55073f23b1dc389c50c81f572355af73183a5a52d1",
|
||||
"spec_sha256": "d3ff845054ca1eef39ffd28d81a4ba03825f74f3f1000f643c92829252c7c775",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "AP3 완주: session cookie가 BFF의 downstream Bearer가 되기까지",
|
||||
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"outputs": [
|
||||
"ap3-csrf-boundary.svg",
|
||||
"ap3-csrf-boundary.drawio",
|
||||
"ap3-csrf-boundary.mmd",
|
||||
"ap3-csrf-boundary.d2",
|
||||
"ap3-csrf-boundary.dot",
|
||||
"ap3-csrf-boundary.drawio",
|
||||
"ap3-csrf-boundary.excalidraw",
|
||||
"ap3-csrf-boundary.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1411" height="392" viewBox="0 0 1411 392" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP3의 masked CSRF 응답과 raw POST credential</title>
|
||||
<desc id="diagram-description">왼쪽의 BFF CSRF endpoint에서 두 결과가 갈라진다. XSRF-TOKEN cookie에는 raw token이 저장되고 JSON body에는 XOR와 Base64로 masked된 token 및 headerName이 담긴다. 두 결과는 SPA의 POST 조립 단계로 모이지만, JSON에서는 headerName만 사용하고 실제 X-XSRF-TOKEN 값은 document.cookie에서 읽은 raw token이다. POST에는 같은 raw 값을 가진 cookie와 header가 함께 도달하고 Spring CSRF filter가 일치 여부를 확인한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-csrf-boundary","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37","anchor":{"kind":"heading","value":"AP3 완주: session cookie가 BFF의 downstream Bearer가 되기까지","line":681}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap3-csrf-boundary","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"heading","value":"AP3 완주: session cookie가 BFF의 downstream Bearer가 되기까지","line":681}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
@@ -10,16 +10,16 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **브라우저** (participant): AP4_SESSION cookie와 함께 edge endpoint를 호출하는 외부 client. Evidence: L992–L1007.
|
||||
- **Nginx edge** (participant): Internal auth subrequest를 만들고 인증 결과를 allowlisted upstream header와 internal token으로 재조립한다. Evidence: L914–L939, L1009–L1028.
|
||||
- **oauth2-proxy** (participant): AP4_SESSION을 검증하고 인증된 user, email과 필요 시 갱신 cookie를 auth response로 돌려준다. Evidence: L980–L990, L1009–L1015.
|
||||
- **Spring upstream** (participant): Identity header와 internal token을 함께 검증해 allowlisted identity JSON을 반환한다. Evidence: L1017–L1026, L1030–L1063.
|
||||
- **브라우저** (participant): AP4_SESSION cookie와 함께 edge endpoint를 호출하는 외부 client. Evidence: L1032–L1047.
|
||||
- **Nginx edge** (participant): Internal auth subrequest를 만들고 인증 결과를 allowlisted upstream header와 internal token으로 재조립한다. Evidence: L954–L979, L1049–L1068.
|
||||
- **oauth2-proxy** (participant): AP4_SESSION을 검증하고 인증된 user, email과 필요 시 갱신 cookie를 auth response로 돌려준다. Evidence: L1020–L1030, L1049–L1055.
|
||||
- **Spring upstream** (participant): Identity header와 internal token을 함께 검증해 allowlisted identity JSON을 반환한다. Evidence: L1057–L1066, L1070–L1103.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **브라우저 → Nginx edge:** GET /api/edge + AP4_SESSION. Evidence: L992–L999.
|
||||
- **Nginx edge → oauth2-proxy:** internal /oauth2/auth subrequest. Evidence: L922–L939, L1009–L1009.
|
||||
- **oauth2-proxy → Nginx edge:** authenticated user + email. Evidence: L1009–L1015.
|
||||
- **Nginx edge → Spring upstream:** GET /edge/me · trusted headers + internal token. Evidence: L1017–L1026.
|
||||
- **Spring upstream → Nginx edge:** trusted identity JSON. Evidence: L1030–L1048.
|
||||
- **Nginx edge → 브라우저:** pattern + user + email + identityHeader. Evidence: L1039–L1048, L1065–L1076.
|
||||
- **브라우저 → Nginx edge:** GET /api/edge + AP4_SESSION. Evidence: L1032–L1039.
|
||||
- **Nginx edge → oauth2-proxy:** internal /oauth2/auth subrequest. Evidence: L962–L979, L1049–L1049.
|
||||
- **oauth2-proxy → Nginx edge:** authenticated user + email. Evidence: L1049–L1055.
|
||||
- **Nginx edge → Spring upstream:** GET /edge/me · trusted headers + internal token. Evidence: L1057–L1066.
|
||||
- **Spring upstream → Nginx edge:** trusted identity JSON. Evidence: L1070–L1088.
|
||||
- **Nginx edge → 브라우저:** pattern + user + email + identityHeader. Evidence: L1079–L1088, L1105–L1116.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="AP4_SESSION cookie와 함께 edge endpoint를 호출하는 외부 client. | Evidence: L992-L1007" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_browser" value="브라우저" tooltip="AP4_SESSION cookie와 함께 edge endpoint를 호출하는 외부 client. | Evidence: L1032-L1047" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_nginx" value="Nginx edge" tooltip="Internal auth subrequest를 만들고 인증 결과를 allowlisted upstream header와 internal token으로 재조립한다. | Evidence: L914-L939, L1009-L1028" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxCell id="n_nginx" value="Nginx edge" tooltip="Internal auth subrequest를 만들고 인증 결과를 allowlisted upstream header와 internal token으로 재조립한다. | Evidence: L954-L979, L1049-L1068" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_oauth2-proxy" value="oauth2-proxy" tooltip="AP4_SESSION을 검증하고 인증된 user, email과 필요 시 갱신 cookie를 auth response로 돌려준다. | Evidence: L980-L990, L1009-L1015" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_oauth2-proxy" value="oauth2-proxy" tooltip="AP4_SESSION을 검증하고 인증된 user, email과 필요 시 갱신 cookie를 auth response로 돌려준다. | Evidence: L1020-L1030, L1049-L1055" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_spring-upstream" value="Spring upstream" tooltip="Identity header와 internal token을 함께 검증해 allowlisted identity JSON을 반환한다. | Evidence: L1017-L1026, L1030-L1063" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxCell id="n_spring-upstream" value="Spring upstream" tooltip="Identity header와 internal token을 함께 검증해 allowlisted identity JSON을 반환한다. | Evidence: L1057-L1066, L1070-L1103" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="675.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_edge-request" value="GET /api/edge + AP4_SESSION" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_nginx">
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap4-edge-forward-auth-flow",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "18ce47da22142be0655461f3677df193562cb037f936b1a8f3d846665187f300",
|
||||
"spec_sha256": "5a2acba76a1d2eb7d67868f2927104e8730e86c9841272cf87f79aa84b82d53f",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "ap4-edge-forward-auth-flow",
|
||||
"line": 1108
|
||||
"line": 1148
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"ap4-edge-forward-auth-flow.svg",
|
||||
"ap4-edge-forward-auth-flow.drawio",
|
||||
"ap4-edge-forward-auth-flow.mmd",
|
||||
"ap4-edge-forward-auth-flow.d2",
|
||||
"ap4-edge-forward-auth-flow.dot",
|
||||
"ap4-edge-forward-auth-flow.drawio",
|
||||
"ap4-edge-forward-auth-flow.excalidraw",
|
||||
"ap4-edge-forward-auth-flow.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="870" height="542" viewBox="0 0 870 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP4 proxy session에서 trusted identity JSON까지</title>
|
||||
<desc id="diagram-description">브라우저가 AP4_SESSION cookie로 Nginx의 /api/edge를 호출한다. Nginx는 oauth2-proxy의 internal auth endpoint에 subrequest를 보내고 인증된 user와 email 결과를 받는다. 이어서 client가 보낸 동명 header를 사용하지 않고 oauth2-proxy 결과와 Nginx 환경의 internal token으로 /edge/me 요청을 새로 조립한다. Spring controller가 user header와 internal token을 함께 확인해 identity JSON을 만들고 Nginx가 브라우저에 전달한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap4-edge-forward-auth-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap4-edge-forward-auth-flow","line":1108}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap4-edge-forward-auth-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"marker","value":"ap4-edge-forward-auth-flow","line":1148}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -50,34 +50,34 @@
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="870" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="992-1007" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="1032-1047" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">브라우저</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="914-939,1009-1028" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="954-979,1049-1068" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Nginx edge</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="980-990,1009-1015" x="465.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="1020-1030,1049-1055" x="465.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="540.0" y="65.0">oauth2-proxy</text>
|
||||
<line class="lifeline" x1="540.0" y1="99.0" x2="540.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="1017-1026,1030-1063" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="1057-1066,1070-1103" x="675.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="750.0" y="65.0">Spring upstream</text>
|
||||
<line class="lifeline" x1="750.0" y1="99.0" x2="750.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="992-999" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="1032-1039" />
|
||||
<rect class="edge-label-bg" x="115.5" y="114.0" width="219.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. GET /api/edge + AP4_SESSION</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 540.0,202.0" data-evidence="922-939,1009-1009" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="330.0,202.0 540.0,202.0" data-evidence="962-979,1049-1049" />
|
||||
<rect class="edge-label-bg" x="308.8" y="176.0" width="252.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="191.0">2. internal /oauth2/auth subrequest</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="540.0,264.0 330.0,264.0" data-evidence="1009-1015" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="540.0,264.0 330.0,264.0" data-evidence="1049-1055" />
|
||||
<rect class="edge-label-bg" x="328.9" y="238.0" width="212.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="253.0">3. authenticated user + email</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="330.0,326.0 750.0,326.0" data-evidence="1017-1026" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="330.0,326.0 750.0,326.0" data-evidence="1057-1066" />
|
||||
<rect class="edge-label-bg" x="380.0" y="300.0" width="320.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="540.0" y="315.0">4. GET /edge/me · trusted headers + internal token</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,388.0 330.0,388.0" data-evidence="1030-1048" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="750.0,388.0 330.0,388.0" data-evidence="1070-1088" />
|
||||
<rect class="edge-label-bg" x="450.6" y="362.0" width="178.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="540.0" y="377.0">5. trusted identity JSON</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,450.0 120.0,450.0" data-evidence="1039-1048,1065-1076" />
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,450.0 120.0,450.0" data-evidence="1079-1088,1105-1116" />
|
||||
<rect class="edge-label-bg" x="75.3" y="424.0" width="299.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="439.0">6. pattern + user + email + identityHeader</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -2,10 +2,10 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "ap4-edge-trust-architecture",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "8ebbe718a7c7581b0e6d398fbf0e67e0c1ac32ac06e4d1b2f1127ac0a5649935",
|
||||
"spec_sha256": "529f7712a29771d1a1c040e03a7d28ea2853d56dc7426b51967a86f9f128dd69",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37",
|
||||
"document_sha256": "15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "AP4: OAuth를 모르는 upstream 앞에서 신뢰 경로를 만든다",
|
||||
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"outputs": [
|
||||
"ap4-edge-trust-architecture.svg",
|
||||
"ap4-edge-trust-architecture.drawio",
|
||||
"ap4-edge-trust-architecture.mmd",
|
||||
"ap4-edge-trust-architecture.d2",
|
||||
"ap4-edge-trust-architecture.dot",
|
||||
"ap4-edge-trust-architecture.drawio",
|
||||
"ap4-edge-trust-architecture.excalidraw",
|
||||
"ap4-edge-trust-architecture.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1039" height="451" viewBox="0 0 1039 451" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">AP4는 공개 edge에서 session을 정제된 upstream identity로 바꾼다</title>
|
||||
<desc id="diagram-description">왼쪽 외부 브라우저가 AP4_SESSION과 함께 AP4 deployment path의 공개 Nginx entry point를 호출한다. 같은 deployment path에서 Nginx는 host 비공개 oauth2-proxy의 internal /oauth2/auth endpoint에 subrequest를 보내 session을 검증하고 user와 email 결과를 받는다. 이어서 client가 보낸 동명 header를 덮어쓰고 Nginx-owned identity header와 internal token을 host 비공개 Spring upstream의 /edge/me로 전달한다. Upstream은 user header와 internal token을 함께 확인하며 JWT를 직접 입력으로 받지 않는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap4-edge-trust-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37","anchor":{"kind":"heading","value":"AP4: OAuth를 모르는 upstream 앞에서 신뢰 경로를 만든다","line":202}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"ap4-edge-trust-architecture","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"15e7c79412ac05ed39d6f3d0c14de8dda92abe7b402139ac2a90492dfe5e162c","anchor":{"kind":"heading","value":"AP4: OAuth를 모르는 upstream 앞에서 신뢰 경로를 만든다","line":202}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -66,7 +66,7 @@
|
||||
<rect class="edge-label-bg" x="663.2" y="265.2" width="71.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="699.0" y="280.2">identity</text>
|
||||
<g id="node-browser">
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="1018-1039"><circle cx="155.0" cy="208.5" r="11.0" /><line x1="155.0" y1="224.5" x2="155.0" y2="233.5" /><line x1="137.0" y1="234.5" x2="173.0" y2="234.5" /><line x1="155.0" y1="233.5" x2="140.0" y2="250.5" /><line x1="155.0" y1="233.5" x2="170.0" y2="250.5" /></g>
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="1018-1039"><circle cx="155.0" cy="208.5" r="11.0" /><line x1="155.0" y1="224.5" x2="155.0" y2="243.5" /><line x1="137.0" y1="234.5" x2="173.0" y2="234.5" /><line x1="155.0" y1="243.5" x2="140.0" y2="260.5" /><line x1="155.0" y1="243.5" x2="170.0" y2="260.5" /></g>
|
||||
<text class="node-label" x="155.0" y="265.5">Browser</text>
|
||||
</g>
|
||||
<g id="node-nginx">
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,28 @@
|
||||
# Bearer JWT가 검증과 역할 변환을 지나 principal이 된다
|
||||
|
||||
## Alternative text
|
||||
|
||||
Bearer JWT 입력이 JwtDecoder, issuer·시간 검증, audience 검증, role converter를 거쳐 authenticated principal이 되는 검증 사슬.
|
||||
|
||||
## Long description
|
||||
|
||||
왼쪽의 raw Bearer JWT가 NimbusJwtDecoder에서 JWK 서명을 확인한 뒤 issuer와 timestamp validator, 프로젝트의 AudienceValidator를 통과해 validated Jwt가 된다. 이어서 KeycloakRealmRoleConverter가 realm_access.roles를 ROLE_* authority로 바꾸고 authenticated principal이 만들어진다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **Bearer JWT** (data): Resource Server가 Authorization header에서 받은 raw Bearer JWT. Evidence: L354–L359.
|
||||
- **NimbusJwtDecoder** (service): JWK를 이용해 JWT 서명을 검증하는 decoder. Evidence: L359–L360.
|
||||
- **Issuer · Time validators** (service): Expected issuer와 timestamp 조건을 확인하는 default validators. Evidence: L361–L361.
|
||||
- **AudienceValidator** (service): keycloak-pattern-api audience 포함 여부를 확인하는 project validator. Evidence: L362–L370.
|
||||
- **Validated Jwt** (data): 서명, issuer·시간, audience 검증을 통과한 Jwt. Evidence: L363–L363.
|
||||
- **Realm role converter** (service): realm_access.roles 문자열에 ROLE_ prefix를 붙여 authority로 변환한다. Evidence: L364–L370.
|
||||
- **Authenticated principal** (actor): 검증된 JWT와 ROLE_* authorities를 가진 인증 결과. Evidence: L365–L365.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Bearer JWT → NimbusJwtDecoder:** decode. Evidence: L354–L360.
|
||||
- **NimbusJwtDecoder → Issuer · Time validators:** signature ok. Evidence: L360–L361.
|
||||
- **Issuer · Time validators → AudienceValidator:** claims. Evidence: L361–L362.
|
||||
- **AudienceValidator → Validated Jwt:** valid audience. Evidence: L362–L363.
|
||||
- **Validated Jwt → Realm role converter:** realm roles. Evidence: L363–L364.
|
||||
- **Realm role converter → Authenticated principal:** authorities. Evidence: L364–L365.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Bearer JWT가 검증과 역할 변환을 지나 principal이 된다
|
||||
# Question: Resource Server가 받은 Bearer JWT는 어떤 검증과 변환 단계를 지나 authenticated principal이 되는가?
|
||||
direction: down
|
||||
n0: "Bearer JWT" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "NimbusJwtDecoder" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "Issuer · Time validators" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "AudienceValidator" {
|
||||
shape: rectangle
|
||||
}
|
||||
n4: "Validated Jwt" {
|
||||
shape: rectangle
|
||||
}
|
||||
n5: "Realm role converter" {
|
||||
shape: rectangle
|
||||
}
|
||||
n6: "Authenticated principal" {
|
||||
shape: person
|
||||
}
|
||||
n0 -> n1: "decode"
|
||||
n1 -> n2: "signature ok"
|
||||
n2 -> n3: "claims"
|
||||
n3 -> n4: "valid audience"
|
||||
n4 -> n5: "realm roles"
|
||||
n5 -> n6: "authorities"
|
||||
@@ -0,0 +1,18 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="Bearer JWT", shape=box, style="rounded,filled"];
|
||||
n1 [label="NimbusJwtDecoder", shape=box, style="rounded,filled"];
|
||||
n2 [label="Issuer · Time validators", shape=box, style="rounded,filled"];
|
||||
n3 [label="AudienceValidator", shape=box, style="rounded,filled"];
|
||||
n4 [label="Validated Jwt", shape=box, style="rounded,filled"];
|
||||
n5 [label="Realm role converter", shape=box, style="rounded,filled"];
|
||||
n6 [label="Authenticated principal", shape=box, style="rounded,dashed,filled"];
|
||||
n0 -> n1 [label="decode", style=solid];
|
||||
n1 -> n2 [label="signature ok", style=solid];
|
||||
n2 -> n3 [label="claims", style=solid];
|
||||
n3 -> n4 [label="valid audience", style=solid];
|
||||
n4 -> n5 [label="realm roles", style=solid];
|
||||
n5 -> n6 [label="authorities", style=solid];
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="bearer-jwt-validation-chain" name="Bearer JWT가 검증과 역할 변환을 지나 principal이 된다">
|
||||
<mxGraphModel dx="680" dy="2231" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="2231" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_bearer-input" value="Bearer JWT" tooltip="Resource Server가 Authorization header에서 받은 raw Bearer JWT. | Evidence: L354-L359" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="48.5" y="145.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_jwt-decoder" value="NimbusJwtDecoder<br/>JWK signature" tooltip="JWK를 이용해 JWT 서명을 검증하는 decoder. | Evidence: L359-L360" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="455.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_issuer-time" value="Issuer · Time validators" tooltip="Expected issuer와 timestamp 조건을 확인하는 default validators. | Evidence: L361-L361" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="43.5" y="765.0" width="150.0" height="74.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_audience-validator" value="AudienceValidator<br/>keycloak-pattern-api" tooltip="keycloak-pattern-api audience 포함 여부를 확인하는 project validator. | Evidence: L362-L370" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="1075.0" width="174.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_validated-jwt" value="Validated Jwt" tooltip="서명, issuer·시간, audience 검증을 통과한 Jwt. | Evidence: L363-L363" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="48.5" y="1409.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_role-converter" value="Realm role converter<br/>KeycloakRealmRoleConverter" tooltip="realm_access.roles 문자열에 ROLE_ prefix를 붙여 authority로 변환한다. | Evidence: L364-L370" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="1719.0" width="216.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_principal" value="Authenticated principal<br/>ROLE_* authorities" tooltip="검증된 JWT와 ROLE_* authorities를 가진 인증 결과. | Evidence: L365-L365" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
|
||||
<mxGeometry x="35.0" y="2095.0" width="160.0" height="91.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_e1" value="decode" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_bearer-input" target="n_jwt-decoder">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="121.8" y="304.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e2" value="signature ok" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_jwt-decoder" target="n_issuer-time">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="119.2" y="617.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e3" value="claims" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_issuer-time" target="n_audience-validator">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="125.2" y="929.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e4" value="valid audience" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_audience-validator" target="n_validated-jwt">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="127.8" y="1249.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e5" value="realm roles" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_validated-jwt" target="n_role-converter">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="138.2" y="1568.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e6" value="authorities" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_role-converter" target="n_principal">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="134.0" y="1914.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "bearer-jwt-validation-chain",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "0b60ebe72b75b64466aeed4bbcfa2b08f0b01d0ef3001c51d84e05885d11eaa9",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak/final/document.md",
|
||||
"document_sha256": "ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "AP1 완주: callback code가 브라우저 Bearer 요청이 되기까지",
|
||||
"line": 227
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"bearer-jwt-validation-chain.svg",
|
||||
"bearer-jwt-validation-chain.mmd",
|
||||
"bearer-jwt-validation-chain.d2",
|
||||
"bearer-jwt-validation-chain.dot",
|
||||
"bearer-jwt-validation-chain.drawio",
|
||||
"bearer-jwt-validation-chain.excalidraw",
|
||||
"bearer-jwt-validation-chain.alt.md"
|
||||
],
|
||||
"lint_issue_count": 1,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
%% Bearer JWT가 검증과 역할 변환을 지나 principal이 된다
|
||||
%% question: Resource Server가 받은 Bearer JWT는 어떤 검증과 변환 단계를 지나 authenticated principal이 되는가?
|
||||
flowchart TB
|
||||
n0["Bearer JWT"]
|
||||
n1["NimbusJwtDecoder"]
|
||||
n2["Issuer · Time validators"]
|
||||
n3["AudienceValidator"]
|
||||
n4["Validated Jwt"]
|
||||
n5["Realm role converter"]
|
||||
n6(["Authenticated principal"])
|
||||
n0 -->|"decode"| n1
|
||||
n1 -->|"signature ok"| n2
|
||||
n2 -->|"claims"| n3
|
||||
n3 -->|"valid audience"| n4
|
||||
n4 -->|"realm roles"| n5
|
||||
n5 -->|"authorities"| n6
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n6 external
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="2231" viewBox="0 0 680 2231" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">Bearer JWT가 검증과 역할 변환을 지나 principal이 된다</title>
|
||||
<desc id="diagram-description">왼쪽의 raw Bearer JWT가 NimbusJwtDecoder에서 JWK 서명을 확인한 뒤 issuer와 timestamp validator, 프로젝트의 AudienceValidator를 통과해 validated Jwt가 된다. 이어서 KeycloakRealmRoleConverter가 realm_access.roles를 ROLE_* authority로 바꾸고 authenticated principal이 만들어진다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"bearer-jwt-validation-chain","profile":"component-flow"},"source_context":{"document":"docs/keycloak/final/document.md","document_sha256":"ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9","anchor":{"kind":"heading","value":"AP1 완주: callback code가 브라우저 Bearer 요청이 되기까지","line":227}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="2231" />
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="123.5,209.0 123.5,332.0 120.0,332.0 120.0,455.0" data-evidence="354-360" />
|
||||
<rect class="edge-label-bg" x="92.7" y="290.0" width="58.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="121.8" y="305.0">decode</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="120.0,526.0 120.0,645.5 118.5,645.5 118.5,765.0" data-evidence="360-361" />
|
||||
<rect class="edge-label-bg" x="70.0" y="603.5" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="119.2" y="618.5">signature ok</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="118.5,839.0 118.5,957.0 132.0,957.0 132.0,1075.0" data-evidence="361-362" />
|
||||
<rect class="edge-label-bg" x="96.2" y="915.0" width="58.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="125.2" y="930.0">claims</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="132.0,1146.0 132.0,1277.5 123.5,1277.5 123.5,1409.0" data-evidence="362-363" />
|
||||
<rect class="edge-label-bg" x="71.8" y="1235.5" width="111.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="127.8" y="1250.5">valid audience</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="123.5,1473.0 123.5,1596.0 153.0,1596.0 153.0,1719.0" data-evidence="363-364" />
|
||||
<rect class="edge-label-bg" x="92.4" y="1554.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="138.2" y="1569.0">realm roles</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="153.0,1790.0 153.0,1942.5 115.0,1942.5 115.0,2095.0" data-evidence="364-365" />
|
||||
<rect class="edge-label-bg" x="88.2" y="1900.5" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="134.0" y="1915.5">authorities</text>
|
||||
<g id="node-bearer-input">
|
||||
<rect class="node-shape kind-data emphasis-normal role-source" data-evidence="354-359" x="48.5" y="145.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="123.5" y="175.0">Bearer JWT</text>
|
||||
</g>
|
||||
<g id="node-jwt-decoder">
|
||||
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="359-360" x="45.0" y="455.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="482.0">NimbusJwtDecoder</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="503.0" x2="181.0" y2="503.0" />
|
||||
<text class="node-detail" x="61.0" y="520.0">JWK signature</text>
|
||||
</g>
|
||||
<g id="node-issuer-time">
|
||||
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="361-361" x="43.5" y="765.0" width="150.0" height="74.0" rx="7" />
|
||||
<text class="node-label" x="118.5" y="792.0">Issuer · Time</text>
|
||||
<text class="node-label" x="118.5" y="810.0">validators</text>
|
||||
</g>
|
||||
<g id="node-audience-validator">
|
||||
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="362-370" x="45.0" y="1075.0" width="174.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="132.0" y="1102.0">AudienceValidator</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="1123.0" x2="205.0" y2="1123.0" />
|
||||
<text class="node-detail" x="61.0" y="1140.0">keycloak-pattern-api</text>
|
||||
</g>
|
||||
<g id="node-validated-jwt">
|
||||
<rect class="node-shape kind-data emphasis-normal role-service" data-evidence="363-363" x="48.5" y="1409.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="123.5" y="1439.0">Validated Jwt</text>
|
||||
</g>
|
||||
<g id="node-role-converter">
|
||||
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="364-370" x="45.0" y="1719.0" width="216.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="153.0" y="1746.0">Realm role converter</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="1767.0" x2="247.0" y2="1767.0" />
|
||||
<text class="node-detail" x="61.0" y="1784.0">KeycloakRealmRoleConverter</text>
|
||||
</g>
|
||||
<g id="node-principal">
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="365-365"><circle cx="115.0" cy="2115.0" r="11.0" /><line x1="115.0" y1="2131.0" x2="115.0" y2="2157.0" /><line x1="97.0" y1="2141.0" x2="133.0" y2="2141.0" /><line x1="115.0" y1="2157.0" x2="100.0" y2="2174.0" /><line x1="115.0" y1="2157.0" x2="130.0" y2="2174.0" /></g>
|
||||
<text class="node-label" x="115.0" y="2161.0">Authenticated</text>
|
||||
<text class="node-label" x="115.0" y="2179.0">principal</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,24 @@
|
||||
# 외부 IdP 인증은 Keycloak broker에서 application credential로 다시 경계가 나뉜다
|
||||
|
||||
## Alternative text
|
||||
|
||||
Upstream IdP zone의 Google identity assertion이 Keycloak broker zone의 validation과 account linking을 거쳐 Keycloak authorization code가 되고 기존 AP1~AP4 application 경계로 이어지는 다이어그램.
|
||||
|
||||
## Long description
|
||||
|
||||
왼쪽 upstream IdP zone의 Google이 identity assertion을 Keycloak broker에 넘긴다. Keycloak은 assertion을 검증하고 provider alias와 upstream sub로 local identity를 연결한 뒤 자기 authorization code를 발급한다. 이 code가 기존 AP1·AP2·AP3·AP4 application 경계 중 하나로 이어지므로 application이 신뢰하는 issuer는 계속 Keycloak이다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **Boundary: Upstream IdP** (system): Keycloak 앞에서 사용자 인증을 수행하는 외부 identity provider 경계. Evidence: L1152–L1157.
|
||||
- **Boundary: Keycloak → Application** (system): Keycloak이 upstream 결과를 local identity와 자기 authorization code로 바꿔 application OAuth 경계로 넘기는 구간. Evidence: L1152–L1165.
|
||||
- **Google IdP** (service): Upstream authorization을 수행하고 identity assertion을 Keycloak에 돌려주는 IdP. Evidence: L1152–L1157.
|
||||
- **Keycloak broker** (service): Upstream response를 검증하고 provider alias와 upstream sub로 local user와 session을 연결하는 broker. Evidence: L1152–L1160.
|
||||
- **Keycloak authorization code** (data): Upstream token이 아니라 Keycloak이 downstream application에 발급하는 authorization code. Evidence: L1161–L1165.
|
||||
- **AP1 · AP2 · AP3 · AP4** (service): 외부 IdP와 무관하게 기존 application credential boundary 중 하나가 이어지는 downstream 경계. Evidence: L1162–L1165.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Google IdP → Keycloak broker:** identity assertion. Evidence: L1157–L1158.
|
||||
- **Keycloak broker → Keycloak authorization code:** issue code. Evidence: L1158–L1161.
|
||||
- **Keycloak authorization code → AP1 · AP2 · AP3 · AP4:** Keycloak issuer. Evidence: L1161–L1165.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 외부 IdP 인증은 Keycloak broker에서 application credential로 다시 경계가 나뉜다
|
||||
# Question: 외부 IdP의 인증 결과는 Keycloak broker에서 어떻게 local identity와 Keycloak-issued authorization code로 바뀌고 기존 AP1~AP4 경계로 이어지는가?
|
||||
direction: right
|
||||
g0: "Upstream IdP" {
|
||||
n0: "Google IdP" {
|
||||
shape: rectangle
|
||||
}
|
||||
}
|
||||
g1: "Keycloak → Application" {
|
||||
n1: "Keycloak broker" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "Keycloak authorization code" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "AP1 · AP2 · AP3 · AP4" {
|
||||
shape: rectangle
|
||||
}
|
||||
}
|
||||
g0.n0 -> g1.n1: "identity assertion"
|
||||
g1.n1 -> g1.n2: "issue code"
|
||||
g1.n2 -> g1.n3: "Keycloak issuer"
|
||||
@@ -0,0 +1,22 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
subgraph cluster_0 {
|
||||
label="Upstream IdP";
|
||||
style="rounded,dashed";
|
||||
color="#66788a";
|
||||
n0 [label="Google IdP", shape=box, style="rounded,filled"];
|
||||
}
|
||||
subgraph cluster_1 {
|
||||
label="Keycloak → Application";
|
||||
style="rounded,dashed";
|
||||
color="#66788a";
|
||||
n1 [label="Keycloak broker", shape=box, style="rounded,filled"];
|
||||
n2 [label="Keycloak authorization code", shape=box, style="rounded,filled"];
|
||||
n3 [label="AP1 · AP2 · AP3 · AP4", shape=box, style="rounded,filled"];
|
||||
}
|
||||
n0 -> n1 [label="identity assertion", style=solid];
|
||||
n1 -> n2 [label="issue code", style=solid];
|
||||
n2 -> n3 [label="Keycloak issuer", style=solid];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="idp-broker-upstream-downstream-boundary" name="외부 IdP 인증은 Keycloak broker에서 application credential로 다시 경계가 나뉜다">
|
||||
<mxGraphModel dx="1080" dy="395" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1080" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="g_upstream-zone" value="Upstream IdP" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="49.0" width="250.0" height="143.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="g_broker-app-zone" value="Keycloak → Application" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="565.0" y="49.0" width="470.0" height="281.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_google" value="Google IdP<br/>identity assertion" tooltip="Upstream authorization을 수행하고 identity assertion을 Keycloak에 돌려주는 IdP. | Evidence: L1152-L1157" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="75.0" y="95.0" width="190.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_broker" value="Keycloak broker<br/>broker validation<br/>provider alias + sub<br/>local user · session" tooltip="Upstream response를 검증하고 provider alias와 upstream sub로 local user와 session을 연결하는 broker. | Evidence: L1152-L1160" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="595.0" y="95.0" width="190.0" height="105.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_keycloak-code" value="Keycloak authorization code" tooltip="Upstream token이 아니라 Keycloak이 downstream application에 발급하는 authorization code. | Evidence: L1161-L1165" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="815.0" y="95.0" width="190.0" height="74.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_downstream" value="AP1 · AP2 · AP3 · AP4" tooltip="외부 IdP와 무관하게 기존 application credential boundary 중 하나가 이어지는 downstream 경계. | Evidence: L1162-L1165" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="595.0" y="240.0" width="190.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_b1" value="identity assertion" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_google" target="n_broker">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="454.0" y="139.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_b2" value="issue code" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_broker" target="n_keycloak-code">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="812.2" y="31.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_b3" value="Keycloak issuer" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak-code" target="n_downstream">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="867.5" y="322.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,732 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "group-upstream-zone",
|
||||
"type": "rectangle",
|
||||
"x": 45.0,
|
||||
"y": 49.0,
|
||||
"width": 250.0,
|
||||
"height": 143.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 843220907,
|
||||
"version": 1,
|
||||
"versionNonce": 1715574101,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "group-label-upstream-zone",
|
||||
"type": "text",
|
||||
"x": 61.0,
|
||||
"y": 55.0,
|
||||
"width": 108,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 73837189,
|
||||
"version": 1,
|
||||
"versionNonce": 1087599900,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 14,
|
||||
"fontFamily": 5,
|
||||
"text": "Upstream IdP",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Upstream IdP",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "group-broker-app-zone",
|
||||
"type": "rectangle",
|
||||
"x": 565.0,
|
||||
"y": 49.0,
|
||||
"width": 470.0,
|
||||
"height": 281.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 843169464,
|
||||
"version": 1,
|
||||
"versionNonce": 766837970,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "group-label-broker-app-zone",
|
||||
"type": "text",
|
||||
"x": 581.0,
|
||||
"y": 55.0,
|
||||
"width": 198,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1618498892,
|
||||
"version": 1,
|
||||
"versionNonce": 1933137034,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 14,
|
||||
"fontFamily": 5,
|
||||
"text": "Keycloak → Application",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Keycloak → Application",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-b1",
|
||||
"type": "arrow",
|
||||
"x": 265.0,
|
||||
"y": 130.5,
|
||||
"width": 330.0,
|
||||
"height": 17.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1881077425,
|
||||
"version": 1,
|
||||
"versionNonce": 1053322627,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
165.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
165.0,
|
||||
17.0
|
||||
],
|
||||
[
|
||||
330.0,
|
||||
17.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-google",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-broker",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-b1",
|
||||
"type": "text",
|
||||
"x": 382.0,
|
||||
"y": 127.0,
|
||||
"width": 144,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1939821942,
|
||||
"version": 1,
|
||||
"versionNonce": 1675635312,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "identity assertion",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "identity assertion",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-b2",
|
||||
"type": "arrow",
|
||||
"x": 785.0,
|
||||
"y": 59.0,
|
||||
"width": 30.0,
|
||||
"height": 88.5,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1090225723,
|
||||
"version": 1,
|
||||
"versionNonce": 719927994,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
88.5
|
||||
],
|
||||
[
|
||||
30.0,
|
||||
88.5
|
||||
],
|
||||
[
|
||||
30.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
64.0
|
||||
],
|
||||
[
|
||||
30.0,
|
||||
64.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-broker",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-keycloak-code",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-b2",
|
||||
"type": "text",
|
||||
"x": 767.25,
|
||||
"y": 19.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 92689614,
|
||||
"version": 1,
|
||||
"versionNonce": 938215730,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "issue code",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "issue code",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-b3",
|
||||
"type": "arrow",
|
||||
"x": 690.0,
|
||||
"y": 169.0,
|
||||
"width": 220.0,
|
||||
"height": 181.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 920777401,
|
||||
"version": 1,
|
||||
"versionNonce": 1003519303,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
220.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
220.0,
|
||||
181.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
181.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
135.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-keycloak-code",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-downstream",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-b3",
|
||||
"type": "text",
|
||||
"x": 807.5,
|
||||
"y": 310.0,
|
||||
"width": 120,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 48429281,
|
||||
"version": 1,
|
||||
"versionNonce": 1797138124,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "Keycloak issuer",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Keycloak issuer",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-google",
|
||||
"type": "rectangle",
|
||||
"x": 75.0,
|
||||
"y": 95.0,
|
||||
"width": 190.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1505940743,
|
||||
"version": 1,
|
||||
"versionNonce": 397167959,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-google",
|
||||
"type": "text",
|
||||
"x": 85.0,
|
||||
"y": 105.0,
|
||||
"width": 170.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1045077956,
|
||||
"version": 1,
|
||||
"versionNonce": 1546820915,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Google IdP\nidentity assertion",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Google IdP\nidentity assertion",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-broker",
|
||||
"type": "rectangle",
|
||||
"x": 595.0,
|
||||
"y": 95.0,
|
||||
"width": 190.0,
|
||||
"height": 105.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 21942044,
|
||||
"version": 1,
|
||||
"versionNonce": 1744191102,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-broker",
|
||||
"type": "text",
|
||||
"x": 605.0,
|
||||
"y": 105.0,
|
||||
"width": 170.0,
|
||||
"height": 85.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1714928728,
|
||||
"version": 1,
|
||||
"versionNonce": 423573481,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Keycloak broker\nbroker validation\nprovider alias + sub\nlocal user · session",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Keycloak broker\nbroker validation\nprovider alias + sub\nlocal user · session",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-keycloak-code",
|
||||
"type": "rectangle",
|
||||
"x": 815.0,
|
||||
"y": 95.0,
|
||||
"width": 190.0,
|
||||
"height": 74.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1259207617,
|
||||
"version": 1,
|
||||
"versionNonce": 382015936,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-keycloak-code",
|
||||
"type": "text",
|
||||
"x": 825.0,
|
||||
"y": 105.0,
|
||||
"width": 170.0,
|
||||
"height": 54.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1761782804,
|
||||
"version": 1,
|
||||
"versionNonce": 78085105,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Keycloak authorization code",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Keycloak authorization code",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-downstream",
|
||||
"type": "rectangle",
|
||||
"x": 595.0,
|
||||
"y": 240.0,
|
||||
"width": 190.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 422578816,
|
||||
"version": 1,
|
||||
"versionNonce": 1949897943,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-downstream",
|
||||
"type": "text",
|
||||
"x": 605.0,
|
||||
"y": 250.0,
|
||||
"width": 170.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 74338888,
|
||||
"version": 1,
|
||||
"versionNonce": 790089632,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "AP1 · AP2 · AP3 · AP4",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "AP1 · AP2 · AP3 · AP4",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "idp-broker-upstream-downstream-boundary",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "9bb6a11aba2502fbe9ea9c0a0519073a1a04b9b3cae944720d592ffdc3be9511",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak/final/document.md",
|
||||
"document_sha256": "ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "Google login이 들어와도 네 애플리케이션 경계는 바뀌지 않는다",
|
||||
"line": 1150
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"idp-broker-upstream-downstream-boundary.svg",
|
||||
"idp-broker-upstream-downstream-boundary.mmd",
|
||||
"idp-broker-upstream-downstream-boundary.d2",
|
||||
"idp-broker-upstream-downstream-boundary.dot",
|
||||
"idp-broker-upstream-downstream-boundary.drawio",
|
||||
"idp-broker-upstream-downstream-boundary.excalidraw",
|
||||
"idp-broker-upstream-downstream-boundary.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "two-zone-pipeline",
|
||||
"reference_ids": [
|
||||
"localization-pipeline"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
%% 외부 IdP 인증은 Keycloak broker에서 application credential로 다시 경계가 나뉜다
|
||||
%% question: 외부 IdP의 인증 결과는 Keycloak broker에서 어떻게 local identity와 Keycloak-issued authorization code로 바뀌고 기존 AP1~AP4 경계로 이어지는가?
|
||||
flowchart LR
|
||||
subgraph g_upstream_zone["Upstream IdP"]
|
||||
n0["Google IdP"]
|
||||
end
|
||||
subgraph g_broker_app_zone["Keycloak → Application"]
|
||||
n1["Keycloak broker"]
|
||||
n2["Keycloak authorization code"]
|
||||
n3["AP1 · AP2 · AP3 · AP4"]
|
||||
end
|
||||
n0 -->|"identity assertion"| n1
|
||||
n1 -->|"issue code"| n2
|
||||
n2 -->|"Keycloak issuer"| n3
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="395" viewBox="0 0 1080 395" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">외부 IdP 인증은 Keycloak broker에서 application credential로 다시 경계가 나뉜다</title>
|
||||
<desc id="diagram-description">왼쪽 upstream IdP zone의 Google이 identity assertion을 Keycloak broker에 넘긴다. Keycloak은 assertion을 검증하고 provider alias와 upstream sub로 local identity를 연결한 뒤 자기 authorization code를 발급한다. 이 code가 기존 AP1·AP2·AP3·AP4 application 경계 중 하나로 이어지므로 application이 신뢰하는 issuer는 계속 Keycloak이다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"idp-broker-upstream-downstream-boundary","profile":"two-zone-pipeline"},"source_context":{"document":"docs/keycloak/final/document.md","document_sha256":"ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9","anchor":{"kind":"heading","value":"Google login이 들어와도 네 애플리케이션 경계는 바뀌지 않는다","line":1150}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1080" height="395" />
|
||||
<rect class="group-box" x="45.0" y="49.0" width="250.0" height="143.0" rx="8" />
|
||||
<rect class="group-label-bg" x="59.0" y="39.0" width="106.0" height="22" />
|
||||
<text class="group-label" x="69.0" y="54.0">Upstream IdP</text>
|
||||
<rect class="group-box" x="565.0" y="49.0" width="470.0" height="281.0" rx="8" />
|
||||
<rect class="group-label-bg" x="579.0" y="39.0" width="176.0" height="22" />
|
||||
<text class="group-label" x="589.0" y="54.0">Keycloak → Application</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="265.0,130.5 430.0,130.5 430.0,147.5 595.0,147.5" data-evidence="1157-1158" />
|
||||
<rect class="edge-label-bg" x="384.7" y="125.0" width="138.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="454.0" y="140.0">identity assertion</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="785.0,147.5 815.0,147.5 815.0,59.0 785.0,59.0 785.0,123.0 815.0,123.0" data-evidence="1158-1161" />
|
||||
<rect class="edge-label-bg" x="769.8" y="17.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="812.2" y="32.0">issue code</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="910.0,169.0 910.0,350.0 690.0,350.0 690.0,304.0" data-evidence="1161-1165" />
|
||||
<rect class="edge-label-bg" x="808.2" y="308.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="867.5" y="323.0">Keycloak issuer</text>
|
||||
<g id="node-google">
|
||||
<rect class="node-shape kind-service emphasis-normal role-source" data-evidence="1152-1157" x="75.0" y="95.0" width="190.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="170.0" y="122.0">Google IdP</text>
|
||||
<line class="node-detail-divider" x1="89.0" y1="143.0" x2="251.0" y2="143.0" />
|
||||
<text class="node-detail" x="91.0" y="160.0">identity assertion</text>
|
||||
</g>
|
||||
<g id="node-broker">
|
||||
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="1152-1160" x="595.0" y="95.0" width="190.0" height="105.0" rx="7" />
|
||||
<text class="node-label" x="690.0" y="122.0">Keycloak broker</text>
|
||||
<line class="node-detail-divider" x1="609.0" y1="143.0" x2="771.0" y2="143.0" />
|
||||
<text class="node-detail" x="611.0" y="160.0">broker validation</text>
|
||||
<text class="node-detail" x="611.0" y="176.0">provider alias + sub</text>
|
||||
<text class="node-detail" x="611.0" y="192.0">local user · session</text>
|
||||
</g>
|
||||
<g id="node-keycloak-code">
|
||||
<rect class="node-shape kind-data emphasis-normal role-service" data-evidence="1161-1165" x="815.0" y="95.0" width="190.0" height="74.0" rx="7" />
|
||||
<text class="node-label" x="910.0" y="122.0">Keycloak authorization</text>
|
||||
<text class="node-label" x="910.0" y="140.0">code</text>
|
||||
</g>
|
||||
<g id="node-downstream">
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="1162-1165" x="595.0" y="240.0" width="190.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="690.0" y="270.0">AP1 · AP2 · AP3 · AP4</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -2,10 +2,10 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "login-api-phase-split",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "5895a0f2bfe3d0fe1ced46147651689dc65c3488695300029ffaedf07490db71",
|
||||
"spec_sha256": "6c1a6b2d0a4abd9435ab2e0d7cc1981f5d1ecf2812ccc507dc5e7d33000df371",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37",
|
||||
"document_sha256": "ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "로그인 흐름과 API 흐름은 같은 선이 아니다",
|
||||
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"outputs": [
|
||||
"login-api-phase-split.svg",
|
||||
"login-api-phase-split.drawio",
|
||||
"login-api-phase-split.mmd",
|
||||
"login-api-phase-split.d2",
|
||||
"login-api-phase-split.dot",
|
||||
"login-api-phase-split.drawio",
|
||||
"login-api-phase-split.excalidraw",
|
||||
"login-api-phase-split.alt.md"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1235" height="539" viewBox="0 0 1235 539" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">로그인 credential owner와 API caller는 같은 축이 아니다</title>
|
||||
<desc id="diagram-description">왼쪽 로그인 구간에는 Keycloak과 AP2 mediator, AP3 BFF, AP4 oauth2-proxy가 있다. Keycloak의 authorization code는 각 OAuth client 쪽으로 이동한다. 오른쪽 애플리케이션 요청 구간에는 AP2 브라우저, AP3 BFF, AP4 Nginx와 보호 자원이 있다. AP2는 token을 받는 mediator와 API를 호출하는 브라우저가 갈리고, AP3는 같은 BFF가 token 발급과 API 호출을 소유한다. AP4는 oauth2-proxy가 code 교환과 session 검증을 맡고 Nginx가 upstream 요청 연결과 identity header 조립을 맡는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"login-api-phase-split","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"e2cc061f683294c937f089ad874cbf7a617f7313c07a92c2148fb11287a0da37","anchor":{"kind":"heading","value":"로그인 흐름과 API 흐름은 같은 선이 아니다","line":31}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"login-api-phase-split","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"ea10df24b892e2c57123a37a4b4f0d821e4f394353e6746f50df6a48342353e9","anchor":{"kind":"heading","value":"로그인 흐름과 API 흐름은 같은 선이 아니다","line":31}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
@@ -107,7 +107,7 @@
|
||||
<text class="node-detail" x="396.0" y="454.5">AP4_SESSION 검증</text>
|
||||
</g>
|
||||
<g id="node-ap2-browser-caller">
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="37-37"><circle cx="770.0" cy="101.0" r="11.0" /><line x1="770.0" y1="117.0" x2="770.0" y2="126.0" /><line x1="752.0" y1="127.0" x2="788.0" y2="127.0" /><line x1="770.0" y1="126.0" x2="755.0" y2="143.0" /><line x1="770.0" y1="126.0" x2="785.0" y2="143.0" /></g>
|
||||
<g class="actor-symbol emphasis-normal" data-evidence="37-37"><circle cx="770.0" cy="101.0" r="11.0" /><line x1="770.0" y1="117.0" x2="770.0" y2="136.0" /><line x1="752.0" y1="127.0" x2="788.0" y2="127.0" /><line x1="770.0" y1="136.0" x2="755.0" y2="153.0" /><line x1="770.0" y1="136.0" x2="785.0" y2="153.0" /></g>
|
||||
<text class="node-label" x="770.0" y="158.0">AP2 브라우저</text>
|
||||
</g>
|
||||
<g id="node-ap3-bff-caller">
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |