chore: 문서를 작성할 때 한국어의 표현 작성 스킬 추가 및 1인칭 관점의 글 작성 검증 테스트 추가

This commit is contained in:
DongHyeonka
2026-07-29 16:48:03 +09:00
parent c39406bbdd
commit 41501b5d06
520 changed files with 95494 additions and 2231 deletions
@@ -0,0 +1,24 @@
# AP1 callback code에서 브라우저 Bearer 요청까지
## Alternative text
브라우저 SPA, Keycloak, Resource Server 사이에서 authorization request, callback, token 교환, Bearer API 호출과 JSON 응답이 이어지는 순서도.
## Long 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을 받는다.
## Elements and evidence
- **브라우저 SPA** (participant): PKCE transaction과 token set을 처리하고 access token으로 API 요청을 만드는 public client. Evidence: L201L236, L240L280, L303L322.
- **Keycloak** (participant): Authorization code를 발급하고 code와 verifier를 token set으로 교환하는 authorization server. Evidence: L223L263.
- **Resource Server** (participant): Bearer JWT의 signature, issuer, timestamp와 audience를 검증하고 사용자 JSON을 만드는 Spring API. Evidence: L311L353.
## Relationships
- **브라우저 SPA → Keycloak:** authorization request · S256 challenge. Evidence: L223L236.
- **Keycloak → 브라우저 SPA:** callback · code + state. Evidence: L240L250.
- **브라우저 SPA → Keycloak:** token request · code + verifier. Evidence: L250L261.
- **Keycloak → 브라우저 SPA:** access + refresh + ID token. Evidence: L263L280.
- **브라우저 SPA → Resource Server:** GET /api/me · Bearer access token. Evidence: L303L322.
- **Resource Server → 브라우저 SPA:** subject · username · issuer · audience. Evidence: L342L376.
@@ -0,0 +1,18 @@
# AP1 callback code에서 브라우저 Bearer 요청까지
# Question: AP1에서 authorization code는 어떤 순서로 브라우저의 Bearer API 요청과 사용자 JSON이 되는가?
direction: right
n0: "브라우저 SPA" {
shape: rectangle
}
n1: "Keycloak" {
shape: rectangle
}
n2: "Resource Server" {
shape: rectangle
}
n0 -> n1: "authorization request · S256 challenge"
n1 -> n0: "callback · code + state"
n0 -> n1: "token request · code + verifier"
n1 -> n0: "access + refresh + ID token"
n0 -> n2: "GET /api/me · Bearer access token"
n2 -> n0: "subject · username · issuer · audience"
@@ -0,0 +1,14 @@
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];
n0 [label="브라우저 SPA", shape=box, style="rounded,filled"];
n1 [label="Keycloak", shape=box, style="rounded,filled"];
n2 [label="Resource Server", shape=box, style="rounded,filled"];
n0 -> n1 [label="authorization request · S256 challenge", style=solid];
n1 -> n0 [label="callback · code + state", style=solid];
n0 -> n1 [label="token request · code + verifier", style=solid];
n1 -> n0 [label="access + refresh + ID token", style=solid];
n0 -> n2 [label="GET /api/me · Bearer access token", style=solid];
n2 -> n0 [label="subject · username · issuer · audience", style=solid];
}
@@ -0,0 +1,50 @@
<?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="ap1-browser-bearer-flow" name="AP1 callback code에서 브라우저 Bearer 요청까지">
<mxGraphModel dx="680" dy="542" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<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">
<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">
<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">
<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">
<mxGeometry relative="1" as="geometry">
<mxPoint x="225.0" y="128.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_callback" value="callback · code + state" 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">
<mxGeometry relative="1" as="geometry">
<mxPoint x="225.0" y="190.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_token-request" value="token request · code + verifier" 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">
<mxGeometry relative="1" as="geometry">
<mxPoint x="225.0" y="252.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_token-response" 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">
<mxGeometry relative="1" as="geometry">
<mxPoint x="225.0" y="314.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_api-request" value="GET /api/me · Bearer access token" 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_resource-server">
<mxGeometry relative="1" as="geometry">
<mxPoint x="330.0" y="376.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_api-response" value="subject · username · issuer · audience" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_resource-server" target="n_browser">
<mxGeometry relative="1" as="geometry">
<mxPoint x="330.0" y="438.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,766 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-authorize",
"type": "arrow",
"x": 120.0,
"y": 140.0,
"width": 210.0,
"height": 0.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": 153262066,
"version": 1,
"versionNonce": 1109865188,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
210.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-keycloak",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-authorize",
"type": "text",
"x": 73.0,
"y": 116.0,
"width": 304,
"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": 1306432149,
"version": 1,
"versionNonce": 1642113840,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "authorization request · S256 challenge",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "authorization request · S256 challenge",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-callback",
"type": "arrow",
"x": 120.0,
"y": 202.0,
"width": 210.0,
"height": 0.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": 1802650704,
"version": 1,
"versionNonce": 1991045792,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
210.0,
0.0
],
[
0.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-keycloak",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-callback",
"type": "text",
"x": 133.0,
"y": 178.0,
"width": 184,
"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": 1929639337,
"version": 1,
"versionNonce": 1816804132,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "callback · code + state",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "callback · code + state",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-token-request",
"type": "arrow",
"x": 120.0,
"y": 264.0,
"width": 210.0,
"height": 0.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": 514094626,
"version": 1,
"versionNonce": 942871324,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
210.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-keycloak",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-token-request",
"type": "text",
"x": 101.0,
"y": 240.0,
"width": 248,
"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": 449177001,
"version": 1,
"versionNonce": 1284264247,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "token request · code + verifier",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "token request · code + verifier",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-token-response",
"type": "arrow",
"x": 120.0,
"y": 326.0,
"width": 210.0,
"height": 0.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": 924685759,
"version": 1,
"versionNonce": 1455183840,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
210.0,
0.0
],
[
0.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-keycloak",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-token-response",
"type": "text",
"x": 117.0,
"y": 302.0,
"width": 216,
"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": 686621634,
"version": 1,
"versionNonce": 108833043,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "access + refresh + ID token",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "access + refresh + ID token",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-api-request",
"type": "arrow",
"x": 120.0,
"y": 388.0,
"width": 420.0,
"height": 0.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": 458447436,
"version": 1,
"versionNonce": 226442684,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
420.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-resource-server",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-api-request",
"type": "text",
"x": 198.0,
"y": 364.0,
"width": 264,
"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": 935709184,
"version": 1,
"versionNonce": 1738417618,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "GET /api/me · Bearer access token",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "GET /api/me · Bearer access token",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-api-response",
"type": "arrow",
"x": 120.0,
"y": 450.0,
"width": 420.0,
"height": 0.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": 369211918,
"version": 1,
"versionNonce": 1850377031,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
420.0,
0.0
],
[
0.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-resource-server",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-api-response",
"type": "text",
"x": 178.0,
"y": 426.0,
"width": 304,
"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": 805232354,
"version": 1,
"versionNonce": 214385421,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "subject · username · issuer · audience",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "subject · username · issuer · audience",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-browser",
"type": "rectangle",
"x": 45.0,
"y": 35.0,
"width": 150.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": 1549664096,
"version": 1,
"versionNonce": 397227867,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-browser",
"type": "text",
"x": 55.0,
"y": 45.0,
"width": 130.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": 225442297,
"version": 1,
"versionNonce": 1441680053,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "브라우저 SPA",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "브라우저 SPA",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-keycloak",
"type": "rectangle",
"x": 255.0,
"y": 35.0,
"width": 150.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": 819278378,
"version": 1,
"versionNonce": 1725278844,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-keycloak",
"type": "text",
"x": 265.0,
"y": 45.0,
"width": 130.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": 1265230567,
"version": 1,
"versionNonce": 1115669911,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Keycloak",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Keycloak",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-resource-server",
"type": "rectangle",
"x": 465.0,
"y": 35.0,
"width": 150.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": 972285001,
"version": 1,
"versionNonce": 1174270881,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-resource-server",
"type": "text",
"x": 475.0,
"y": 45.0,
"width": 130.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": 1517556067,
"version": 1,
"versionNonce": 178679525,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Resource Server",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Resource Server",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "ap1-browser-bearer-flow",
"spec_version": "1.1",
"spec_sha256": "b78a7bf6ff5d1085c2063a500077d35ab4decf701d59c3d73dc05ca01512e936",
"source_context": {
"document": "document.md",
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
"anchor": {
"kind": "marker",
"value": "ap1-browser-bearer-flow",
"line": 395
}
},
"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.excalidraw",
"ap1-browser-bearer-flow.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "sequence",
"reference_ids": [
"payment-approval-sequence"
],
"diagram_only": true
}
@@ -0,0 +1,12 @@
%% AP1 callback code에서 브라우저 Bearer 요청까지
%% question: AP1에서 authorization code는 어떤 순서로 브라우저의 Bearer API 요청과 사용자 JSON이 되는가?
sequenceDiagram
participant n0 as 브라우저 SPA
participant n1 as Keycloak
participant n2 as Resource Server
n0->>n1: authorization request · S256 challenge
n1->>n0: callback · code + state
n0->>n1: token request · code + verifier
n1->>n0: access + refresh + ID token
n0->>n2: GET /api/me · Bearer access token
n2->>n0: subject · username · issuer · audience
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;ap1-browser-bearer-flow&quot;,&quot;profile&quot;:&quot;sequence&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;ap1-browser-bearer-flow&quot;,&quot;line&quot;:395}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;: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="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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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" />
<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>

After

Width:  |  Height:  |  Size: 7.1 KiB