feat: 문서 구조 변경 및 tech-visual 스킬 추가

This commit is contained in:
DongHyeonka
2026-09-04 18:20:00 +09:00
parent 43901f0abf
commit 2efb7ee1f2
683 changed files with 61180 additions and 10479 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
[ 819179ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/f363edc8-2c13-4995-acda-934237034a85:0
[ 838640ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/f363edc8-2c13-4995-acda-934237034a85:0
[ 844151ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/a62a78ed-690e-42c3-96f9-c5be849147be:0
@@ -0,0 +1 @@
[ 798ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 463ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,2 @@
[ 361850ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/relation-targets:0
[ 361984ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/public/documents?size=20:0
@@ -0,0 +1,14 @@
[ 830ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 11627ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/catalog?type=RELATION&size=50:0
[ 8253053ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 8255963ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/catalog?type=TOPIC&size=5:0
[ 8658514ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ https://hyeonworks.com/api/v1/studio/assets:0
[10698799ms] [ERROR] Failed to load resource: the server responded with a status of 503 (Service Unavailable) @ https://hyeonworks.com/api/v1/studio/documents/c72656b5-842d-45d9-b5f6-82b66b09d0b9:0
[10730533ms] [ERROR] Failed to load resource: the server responded with a status of 503 (Service Unavailable) @ https://hyeonworks.com/api/v1/studio/documents/c72656b5-842d-45d9-b5f6-82b66b09d0b9:0
[11002221ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11030769ms] [ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11030823ms] [ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11030884ms] [ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11057678ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11079399ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[11116957ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
@@ -0,0 +1,3 @@
[ 1102620ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 1105434ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=50:0
[ 4945295ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 745ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,2 @@
[ 498ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 4347ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 409ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 753ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 454ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 474ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 394ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 511ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 409ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 405ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,7 @@
[ 433ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3860ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=1:0
[ 3774684ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3808546ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3842412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3876280ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3918631ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=1:0
@@ -0,0 +1,5 @@
[ 382ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 14195ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 20287ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=1:0
[ 4862996ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[13845385ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 451ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 364ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,3 @@
[ 226512ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/documents/bf675775-4f3e-4744-8014-f0efff51422a:0
[ 226542ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a:0
[ 226561ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/studio/api/documents/bf675775-4f3e-4744-8014-f0efff51422a:0
@@ -0,0 +1,3 @@
[ 1101067ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/versions:0
[ 1101094ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/revisions:0
[ 1101121ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/history:0
@@ -0,0 +1 @@
[ 403ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,5 @@
[ 536ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 8481ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents:0
[ 8502ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=100:0
[ 8531ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?page=1&size=20:0
[ 8552ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?offset=20&size=20:0
@@ -0,0 +1,4 @@
[ 69356ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents:0
[ 102402ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/taxonomy:0
[ 130390ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/5f4b6000-cb78-400c-bf6e-a25632a4bb40:0
[ 130428ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/taxonomy:0
@@ -0,0 +1,2 @@
[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 6457ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents:0
@@ -0,0 +1,3 @@
[ 181703ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
[ 198929ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7:0
[ 226814ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e:0
@@ -0,0 +1,14 @@
[ 71685ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
[ 89312ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7:0
[ 106715ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e:0
[ 124167ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/ae6c9bea-d3a3-46e1-bbd4-8d580d336394:0
[ 141598ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72:0
[ 159007ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b:0
[ 176455ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/30a37f34-b406-4061-b924-e22e0be0c3bf:0
[ 278135ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
[ 292037ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7:0
[ 306037ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e:0
[ 319789ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/ae6c9bea-d3a3-46e1-bbd4-8d580d336394:0
[ 333675ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72:0
[ 347605ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b:0
[ 361456ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/30a37f34-b406-4061-b924-e22e0be0c3bf:0
@@ -0,0 +1,7 @@
[ 106605ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
[ 122187ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7:0
[ 137779ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e:0
[ 152342ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/ae6c9bea-d3a3-46e1-bbd4-8d580d336394:0
[ 167621ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72:0
[ 182954ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b:0
[ 198214ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/30a37f34-b406-4061-b924-e22e0be0c3bf:0
@@ -0,0 +1,8 @@
[ 61318ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
[ 76535ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7:0
[ 91740ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e:0
[ 106009ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/ae6c9bea-d3a3-46e1-bbd4-8d580d336394:0
[ 121013ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72:0
[ 135981ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b:0
[ 151129ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/30a37f34-b406-4061-b924-e22e0be0c3bf:0
[ 334257ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e:0
@@ -0,0 +1 @@
[ 218ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 3168ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 50ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://127.0.0.1:8931/favicon.ico:0
@@ -0,0 +1 @@
[ 490ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 460ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 986143ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/documents/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 692791ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/documents/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 3904586ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 1990887ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 21177ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 6785ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 7125ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 37096ms] [ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ https://hyeonworks.com/api/v1/studio/documents/32d0be7d-d88e-4760-8d91-35d3a233a99a/preview:0
@@ -0,0 +1 @@
[ 2388748ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?limit=100:0
@@ -0,0 +1 @@
[ 157ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 22350ms] [ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ https://hyeonworks.com/api/v1/studio/cases/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 6065ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
@@ -0,0 +1 @@
[ 147ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 442ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/public/concepts/gesi-jogeon-hwaginyong-imsi-gaenyeom-girok:0
@@ -0,0 +1,4 @@
[ 14188ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/concepts/a949dcdc-a587-411a-ada9-e6787f7920ed:0
[ 23892ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/58c2d5d9-5865-4b2c-b1cc-3c5c955c33dc:0
[ 65404ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/concepts/a949dcdc-a587-411a-ada9-e6787f7920ed:0
[ 1394622ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/taxonomy/topics?limit=50:0
@@ -0,0 +1,16 @@
- generic [ref=f2e3]:
- banner [ref=f2e4]:
- generic [ref=f2e5]: prod
- main [ref=f2e6]:
- heading "Sign in to your account" [level=1] [ref=f2e8]
- generic [ref=f2e12]:
- generic [ref=f2e13]:
- generic [ref=f2e14]: Username or email
- textbox "Username or email" [active] [ref=f2e17]
- generic [ref=f2e18]:
- generic [ref=f2e19]: Password
- generic [ref=f2e21]:
- textbox "Password" [ref=f2e24]
- button "Show password" [ref=f2e26] [cursor=pointer]:
- generic [ref=f2e27]:
- button "Sign In" [ref=f2e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f2e3]:
- banner [ref=f2e4]:
- generic [ref=f2e5]: prod
- main [ref=f2e6]:
- heading "Sign in to your account" [level=1] [ref=f2e8]
- generic [ref=f2e12]:
- generic [ref=f2e13]:
- generic [ref=f2e14]: Username or email
- textbox "Username or email" [ref=f2e17]: hyeonworks
- generic [ref=f2e18]:
- generic [ref=f2e19]: Password
- generic [ref=f2e21]:
- textbox "Password" [active] [ref=f2e24]
- button "Show password" [ref=f2e26] [cursor=pointer]:
- generic [ref=f2e27]:
- button "Sign In" [ref=f2e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f2e3]:
- banner [ref=f2e4]:
- generic [ref=f2e5]: prod
- main [ref=f2e6]:
- heading "Sign in to your account" [level=1] [ref=f2e8]
- generic [ref=f2e12]:
- generic [ref=f2e13]:
- generic [ref=f2e14]: Username or email
- textbox "Username or email" [ref=f2e17]: hyeonworks
- generic [ref=f2e18]:
- generic [ref=f2e19]: Password
- generic [ref=f2e21]:
- textbox "Password" [active] [ref=f2e24]
- button "Show password" [ref=f2e26] [cursor=pointer]:
- generic [ref=f2e27]:
- button "Sign In" [ref=f2e30] [cursor=pointer]
@@ -0,0 +1,85 @@
- generic [ref=f3e3]:
- link "본문으로 건너뛰기" [ref=f3e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f3e5]:
- generic [ref=f3e6]:
- link "TechLog 홈" [ref=f3e8] [cursor=pointer]:
- /url: /
- text: TechLog
- generic [ref=f3e9]:
- button "TechLog 검색 열기" [ref=f3e11] [cursor=pointer]: 검색
- group [ref=f3e12]:
- generic "메뉴" [ref=f3e13] [cursor=pointer]
- main [ref=f3e14]:
- region [ref=f3e15]:
- heading "TechLog" [level=1] [ref=f3e16]
- paragraph [ref=f3e17]: 문제를 재현하고 검증해 운영 가능한 설계로 연결합니다.
- region [ref=f3e18]:
- generic [ref=f3e19]:
- generic [ref=f3e20]:
- paragraph [ref=f3e21]: Index
- heading "최근 기록" [level=2] [ref=f3e22]
- link "모든 기록 탐색" [ref=f3e23] [cursor=pointer]:
- /url: /explore
- list [ref=f3e24]:
- listitem [ref=f3e25]:
- link "RELEASE 2026.08.22 문서를 쓰고 게시하기까지 문서를 쓰고 공개를 하는 과정에서 생기는 버그를 수정하였습니다. TechLog · TechLog" [ref=f3e26] [cursor=pointer]:
- /url: /releases/0.2.0
- generic [ref=f3e27]:
- generic [ref=f3e28]: RELEASE
- time [ref=f3e29]: 2026.08.22
- generic [ref=f3e30]:
- heading "문서를 쓰고 게시하기까지" [level=3] [ref=f3e31]
- paragraph [ref=f3e32]: 문서를 쓰고 공개를 하는 과정에서 생기는 버그를 수정하였습니다.
- paragraph [ref=f3e33]: TechLog · TechLog
- generic [ref=f3e34]:
- listitem [ref=f3e35]:
- link "RELEASE 2026.08.21 첫 공개 공개 사이트와 Studio 작성 흐름을 처음으로 실제 서버에 올렸습니다. TechLog · TechLog" [ref=f3e36] [cursor=pointer]:
- /url: /releases/0.1.0
- generic [ref=f3e37]:
- generic [ref=f3e38]: RELEASE
- time [ref=f3e39]: 2026.08.21
- generic [ref=f3e40]:
- heading "첫 공개" [level=3] [ref=f3e41]
- paragraph [ref=f3e42]: 공개 사이트와 Studio 작성 흐름을 처음으로 실제 서버에 올렸습니다.
- paragraph [ref=f3e43]: TechLog · TechLog
- generic [ref=f3e44]:
- region [ref=f3e45]:
- generic [ref=f3e47]:
- paragraph [ref=f3e48]: Explore
- heading "어떤 맥락으로 읽을까요?" [level=2] [ref=f3e49]
- list [ref=f3e50]:
- listitem [ref=f3e51]:
- link "문제를 따라가며 검증 과정을 읽습니다 Case" [ref=f3e52] [cursor=pointer]:
- /url: /explore/cases
- generic [ref=f3e53]: 문제를 따라가며 검증 과정을 읽습니다
- strong [ref=f3e54]: Case
- generic [ref=f3e55]:
- listitem [ref=f3e56]:
- link "다시 찾을 수 있는 기술 기준을 확인합니다 Reference" [ref=f3e57] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f3e58]: 다시 찾을 수 있는 기술 기준을 확인합니다
- strong [ref=f3e59]: Reference
- generic [ref=f3e60]:
- listitem [ref=f3e61]:
- link "아직 끝나지 않은 판단과 다음 검증을 봅니다 OpenQuestion" [ref=f3e62] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f3e63]: 아직 끝나지 않은 판단과 다음 검증을 봅니다
- strong [ref=f3e64]: OpenQuestion
- generic [ref=f3e65]:
- listitem [ref=f3e66]:
- link "여러 기록을 하나의 시스템 맥락에서 연결합니다 Project" [ref=f3e67] [cursor=pointer]:
- /url: /projects
- generic [ref=f3e68]: 여러 기록을 하나의 시스템 맥락에서 연결합니다
- strong [ref=f3e69]: Project
- generic [ref=f3e70]:
- contentinfo [ref=f3e71]:
- generic [ref=f3e72]:
- generic [ref=f3e73]:
- paragraph [ref=f3e74]: 동현
- paragraph [ref=f3e75]: 문제를 재현하고 검증해 운영 가능한 설계로 연결합니다.
- generic [ref=f3e76]:
- link "프로필" [ref=f3e77] [cursor=pointer]:
- /url: /profile
- link "변경 기록" [ref=f3e78] [cursor=pointer]:
- /url: /releases
@@ -0,0 +1,55 @@
- generic [ref=f4e3]:
- link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f4e5]:
- generic [ref=f4e6]:
- link "TechLog Studio" [ref=f4e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f4e8]: Studio
- navigation "Studio 주 탐색" [ref=f4e10]:
- link "작업본" [ref=f4e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f4e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f4e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f4e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f4e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f4e17]
- main [ref=f4e18]:
- generic [ref=f4e59]:
- generic [ref=f4e60]:
- paragraph [ref=f4e61]: NEW WORKING COPY
- heading "새 문서" [level=1] [ref=f4e62]
- paragraph [ref=f4e63]: 목적에 맞는 기록 종류를 선택하면 빈 작업본을 만들고 바로 편집을 시작합니다.
- generic [ref=f4e64]:
- group "문서 종류" [ref=f4e65]:
- generic [ref=f4e67] [cursor=pointer]:
- radio "Case 문제를 재현하고 검증한 결론을 기록합니다. 문제 · 결론 · 환경 · 재현 · 본문" [checked] [ref=f4e68]
- strong [ref=f4e69]: Case
- generic [ref=f4e70]: 문제를 재현하고 검증한 결론을 기록합니다.
- generic [ref=f4e71]: 문제 · 결론 · 환경 · 재현 · 본문
- generic [ref=f4e72] [cursor=pointer]:
- radio "Reference 반복해서 적용할 기술 기준을 정리합니다. 목적 · 규칙 · 적용 조건 · 예외 · 예시" [ref=f4e73]
- strong [ref=f4e74]: Reference
- generic [ref=f4e75]: 반복해서 적용할 기술 기준을 정리합니다.
- generic [ref=f4e76]: 목적 · 규칙 · 적용 조건 · 예외 · 예시
- generic [ref=f4e77] [cursor=pointer]:
- radio "Question 아직 닫히지 않은 판단과 다음 검증을 관리합니다. 상태 · 사실 · 가정 · 미지수 · 선택지" [ref=f4e78]
- strong [ref=f4e79]: Question
- generic [ref=f4e80]: 아직 닫히지 않은 판단과 다음 검증을 관리합니다.
- generic [ref=f4e81]: 상태 · 사실 · 가정 · 미지수 · 선택지
- generic [ref=f4e82] [cursor=pointer]:
- radio "Decision 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다. 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거" [ref=f4e83]
- strong [ref=f4e84]: Decision
- generic [ref=f4e85]: 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다.
- generic [ref=f4e86]: 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거
- generic [ref=f4e87]:
- button "작업본 만들기" [ref=f4e88]
- paragraph [ref=f4e89]: 이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다.
- paragraph [ref=f4e58]
@@ -0,0 +1,137 @@
- generic [ref=f4e3]:
- link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f4e5]:
- generic [ref=f4e6]:
- link "TechLog Studio" [ref=f4e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f4e8]: Studio
- navigation "Studio 주 탐색" [ref=f4e10]:
- link "작업본" [ref=f4e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f4e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f4e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f4e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f4e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f4e17]
- main [ref=f4e18]:
- generic [ref=f4e90]:
- tablist "문서 편집 화면" [ref=f4e91]:
- tab "편집" [selected] [ref=f4e92]
- tab "즉시 미리보기" [ref=f4e93]
- generic [ref=f4e94]:
- tabpanel "편집" [ref=f4e96]:
- generic [ref=f4e97]:
- paragraph [ref=f4e98]: CASE · VERSION 1
- heading "문서 편집" [level=1] [ref=f4e99]
- paragraph [ref=f4e100]: 제목 없는 작업본
- region [ref=f4e101]:
- generic [ref=f4e102]:
- paragraph [ref=f4e103]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f4e104]
- generic [ref=f4e105]:
- generic [ref=f4e106]:
- generic [ref=f4e107]: 제목
- textbox "제목" [ref=f4e108]
- generic [ref=f4e109]:
- generic [ref=f4e110]: slug
- textbox "slug" [ref=f4e111]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- generic [ref=f4e112]:
- generic [ref=f4e113]: 요약
- textbox "요약" [ref=f4e114]
- generic [ref=f4e115]:
- generic [ref=f4e116]: Topic
- combobox "Topic" [ref=f4e117]:
- option "선택하지 않음" [selected]
- option "OAuth/OIDC 인증 경계"
- generic [ref=f4e118]:
- generic [ref=f4e119]: Project
- combobox "Project" [ref=f4e120]:
- option "미지정" [selected]
- option "Backend Clean Architecture"
- option "KeyCloak Patterns"
- option "Liner N + 1문제"
- group "관계" [ref=f4e121]:
- paragraph [ref=f4e123]: 연결한 공개 기록이 없습니다.
- button "관계 추가" [ref=f4e124]
- region [ref=f4e125]:
- generic [ref=f4e126]:
- paragraph [ref=f4e127]: CASE
- heading "문제와 검증" [level=2] [ref=f4e128]
- generic [ref=f4e129]:
- generic [ref=f4e130]:
- generic [ref=f4e131]: 문제
- textbox "문제" [ref=f4e132]
- generic [ref=f4e133]:
- generic [ref=f4e134]: 결론
- textbox "결론" [ref=f4e135]
- generic [ref=f4e136]:
- generic [ref=f4e137]: 검증 환경
- textbox "검증 환경" [ref=f4e138]
- generic [ref=f4e139]:
- generic [ref=f4e140]: 재현 조건
- textbox "재현 조건" [ref=f4e141]
- generic [ref=f4e142]:
- generic [ref=f4e143]: 마지막 검증일
- textbox "마지막 검증일" [ref=f4e144]
- generic [ref=f4e145]:
- generic [ref=f4e146]: 본문 Markdown
- textbox "본문 Markdown" [ref=f4e147]
- generic [ref=f4e148]:
- paragraph [ref=f4e149]: EVIDENCE
- heading "본문에 Asset 삽입" [level=3] [ref=f4e150]
- paragraph [ref=f4e151]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다.
- generic [ref=f4e152]:
- generic [ref=f4e153]:
- generic [ref=f4e154]: 업로드 종류
- combobox "업로드 종류" [ref=f4e155]:
- option "이미지" [selected]
- option "다이어그램"
- option "첨부파일"
- button "Asset 업로드" [ref=f4e156]
- generic [ref=f4e157]:
- search [ref=f4e158]:
- generic [ref=f4e159]: Asset 검색
- generic [ref=f4e160]:
- searchbox "Asset 검색" [ref=f4e161]
- button "검색" [ref=f4e162]
- generic [ref=f4e163]:
- checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f4e164]
- generic [ref=f4e165]: 삽입할 때 크게 보기 허용
- status [ref=f4e166]: 삽입할 수 있는 Asset 4개
- list [ref=f4e167]:
- listitem [ref=f4e168]:
- button "ap1-custody-v3-6e0376d2" [ref=f4e169]
- button "삭제" [ref=f4e170]
- listitem [ref=f4e171]:
- button "ap1-custody-v2-e110bd98" [ref=f4e172]
- button "삭제" [ref=f4e173]
- listitem [ref=f4e174]:
- button "ap1-credential-custody-f5e0c027" [ref=f4e175]
- button "삭제" [ref=f4e176]
- listitem [ref=f4e177]:
- button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f4e178]
- button "삭제" [ref=f4e179]
- complementary [ref=f4e180]:
- paragraph [ref=f4e181]: WORKING COPY
- heading "작업 상태" [level=2] [ref=f4e182]
- status "편집 상태" [ref=f4e183]: 저장됨
- generic [ref=f4e184]:
- generic [ref=f4e185]:
- term [ref=f4e186]: 저장 버전
- definition [ref=f4e187]: "1"
- generic [ref=f4e188]:
- term [ref=f4e189]: 종류
- definition [ref=f4e190]: CASE
- button "저장" [disabled] [ref=f4e191]
- button "게시" [ref=f4e192]
- paragraph [ref=f4e193]: 불완전한 초안도 저장할 수 있습니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- paragraph [ref=f4e58]: Case 작업본을 만들었습니다.
@@ -0,0 +1,137 @@
- generic [ref=f4e3]:
- link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f4e5]:
- generic [ref=f4e6]:
- link "TechLog Studio" [ref=f4e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f4e8]: Studio
- navigation "Studio 주 탐색" [ref=f4e10]:
- link "작업본" [ref=f4e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f4e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f4e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f4e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f4e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f4e17]
- main [ref=f4e18]:
- generic [ref=f4e90]:
- tablist "문서 편집 화면" [ref=f4e91]:
- tab "편집" [selected] [ref=f4e92]
- tab "즉시 미리보기" [ref=f4e93]
- generic [ref=f4e94]:
- tabpanel "편집" [ref=f4e96]:
- generic [ref=f4e97]:
- paragraph [ref=f4e98]: CASE · VERSION 1
- heading "문서 편집" [level=1] [ref=f4e99]
- paragraph [ref=f4e100]: 제목 없는 작업본
- region [ref=f4e101]:
- generic [ref=f4e102]:
- paragraph [ref=f4e103]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f4e104]
- generic [ref=f4e105]:
- generic [ref=f4e106]:
- generic [ref=f4e107]: 제목
- textbox "제목" [ref=f4e108]
- generic [ref=f4e109]:
- generic [ref=f4e110]: slug
- textbox "slug" [ref=f4e111]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- generic [ref=f4e112]:
- generic [ref=f4e113]: 요약
- textbox "요약" [ref=f4e114]
- generic [ref=f4e115]:
- generic [ref=f4e116]: Topic
- combobox "Topic" [ref=f4e117]:
- option "선택하지 않음" [selected]
- option "OAuth/OIDC 인증 경계"
- generic [ref=f4e118]:
- generic [ref=f4e119]: Project
- combobox "Project" [ref=f4e120]:
- option "미지정" [selected]
- option "Backend Clean Architecture"
- option "KeyCloak Patterns"
- option "Liner N + 1문제"
- group "관계" [ref=f4e121]:
- paragraph [ref=f4e123]: 연결한 공개 기록이 없습니다.
- button "관계 추가" [ref=f4e124]
- region [ref=f4e125]:
- generic [ref=f4e126]:
- paragraph [ref=f4e127]: CASE
- heading "문제와 검증" [level=2] [ref=f4e128]
- generic [ref=f4e129]:
- generic [ref=f4e130]:
- generic [ref=f4e131]: 문제
- textbox "문제" [ref=f4e132]
- generic [ref=f4e133]:
- generic [ref=f4e134]: 결론
- textbox "결론" [ref=f4e135]
- generic [ref=f4e136]:
- generic [ref=f4e137]: 검증 환경
- textbox "검증 환경" [ref=f4e138]
- generic [ref=f4e139]:
- generic [ref=f4e140]: 재현 조건
- textbox "재현 조건" [ref=f4e141]
- generic [ref=f4e142]:
- generic [ref=f4e143]: 마지막 검증일
- textbox "마지막 검증일" [ref=f4e144]
- generic [ref=f4e145]:
- generic [ref=f4e146]: 본문 Markdown
- textbox "본문 Markdown" [ref=f4e147]
- generic [ref=f4e148]:
- paragraph [ref=f4e149]: EVIDENCE
- heading "본문에 Asset 삽입" [level=3] [ref=f4e150]
- paragraph [ref=f4e151]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다.
- generic [ref=f4e152]:
- generic [ref=f4e153]:
- generic [ref=f4e154]: 업로드 종류
- combobox "업로드 종류" [ref=f4e155]:
- option "이미지"
- option "다이어그램" [selected]
- option "첨부파일"
- button "Asset 업로드" [ref=f4e156]
- generic [ref=f4e157]:
- search [ref=f4e158]:
- generic [ref=f4e159]: Asset 검색
- generic [ref=f4e160]:
- searchbox "Asset 검색" [ref=f4e161]
- button "검색" [ref=f4e162]
- generic [ref=f4e163]:
- checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f4e164]
- generic [ref=f4e165]: 삽입할 때 크게 보기 허용
- status [ref=f4e166]: 삽입할 수 있는 Asset 4개
- list [ref=f4e167]:
- listitem [ref=f4e168]:
- button "ap1-custody-v3-6e0376d2" [ref=f4e169]
- button "삭제" [ref=f4e170]
- listitem [ref=f4e171]:
- button "ap1-custody-v2-e110bd98" [ref=f4e172]
- button "삭제" [ref=f4e173]
- listitem [ref=f4e174]:
- button "ap1-credential-custody-f5e0c027" [ref=f4e175]
- button "삭제" [ref=f4e176]
- listitem [ref=f4e177]:
- button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f4e178]
- button "삭제" [ref=f4e179]
- complementary [ref=f4e180]:
- paragraph [ref=f4e181]: WORKING COPY
- heading "작업 상태" [level=2] [ref=f4e182]
- status "편집 상태" [ref=f4e183]: 저장됨
- generic [ref=f4e184]:
- generic [ref=f4e185]:
- term [ref=f4e186]: 저장 버전
- definition [ref=f4e187]: "1"
- generic [ref=f4e188]:
- term [ref=f4e189]: 종류
- definition [ref=f4e190]: CASE
- button "저장" [disabled] [ref=f4e191]
- button "게시" [ref=f4e192]
- paragraph [ref=f4e193]: 불완전한 초안도 저장할 수 있습니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- paragraph [ref=f4e58]: Case 작업본을 만들었습니다.
@@ -0,0 +1,155 @@
- generic [ref=f4e3]:
- link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f4e5]:
- generic [ref=f4e6]:
- link "TechLog Studio" [ref=f4e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f4e8]: Studio
- navigation "Studio 주 탐색" [ref=f4e10]:
- link "작업본" [ref=f4e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f4e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f4e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f4e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f4e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f4e17]
- main [ref=f4e18]:
- generic [ref=f4e90]:
- tablist "문서 편집 화면" [ref=f4e91]:
- tab "편집" [selected] [ref=f4e92]
- tab "즉시 미리보기" [ref=f4e93]
- generic [ref=f4e94]:
- tabpanel "편집" [ref=f4e96]:
- generic [ref=f4e97]:
- paragraph [ref=f4e98]: CASE · VERSION 1
- heading "문서 편집" [level=1] [ref=f4e99]
- paragraph [ref=f4e100]: 제목 없는 작업본
- region [ref=f4e101]:
- generic [ref=f4e102]:
- paragraph [ref=f4e103]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f4e104]
- generic [ref=f4e105]:
- generic [ref=f4e106]:
- generic [ref=f4e107]: 제목
- textbox "제목" [ref=f4e108]
- generic [ref=f4e109]:
- generic [ref=f4e110]: slug
- textbox "slug" [ref=f4e111]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- generic [ref=f4e112]:
- generic [ref=f4e113]: 요약
- textbox "요약" [ref=f4e114]
- generic [ref=f4e115]:
- generic [ref=f4e116]: Topic
- combobox "Topic" [ref=f4e117]:
- option "선택하지 않음" [selected]
- option "OAuth/OIDC 인증 경계"
- generic [ref=f4e118]:
- generic [ref=f4e119]: Project
- combobox "Project" [ref=f4e120]:
- option "미지정" [selected]
- option "Backend Clean Architecture"
- option "KeyCloak Patterns"
- option "Liner N + 1문제"
- group "관계" [ref=f4e121]:
- paragraph [ref=f4e123]: 연결한 공개 기록이 없습니다.
- button "관계 추가" [ref=f4e124]
- region [ref=f4e125]:
- generic [ref=f4e126]:
- paragraph [ref=f4e127]: CASE
- heading "문제와 검증" [level=2] [ref=f4e128]
- generic [ref=f4e129]:
- generic [ref=f4e130]:
- generic [ref=f4e131]: 문제
- textbox "문제" [ref=f4e132]
- generic [ref=f4e133]:
- generic [ref=f4e134]: 결론
- textbox "결론" [ref=f4e135]
- generic [ref=f4e136]:
- generic [ref=f4e137]: 검증 환경
- textbox "검증 환경" [ref=f4e138]
- generic [ref=f4e139]:
- generic [ref=f4e140]: 재현 조건
- textbox "재현 조건" [ref=f4e141]
- generic [ref=f4e142]:
- generic [ref=f4e143]: 마지막 검증일
- textbox "마지막 검증일" [ref=f4e144]
- generic [ref=f4e145]:
- generic [ref=f4e146]: 본문 Markdown
- textbox "본문 Markdown" [ref=f4e147]
- generic [ref=f4e148]:
- paragraph [ref=f4e149]: EVIDENCE
- heading "본문에 Asset 삽입" [level=3] [ref=f4e150]
- paragraph [ref=f4e151]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다.
- generic [ref=f4e152]:
- generic [ref=f4e153]:
- generic [ref=f4e154]: 업로드 종류
- combobox "업로드 종류" [ref=f4e155]:
- option "이미지"
- option "다이어그램" [selected]
- option "첨부파일"
- button "Asset 업로드" [ref=f4e156]
- generic [ref=f4e157]:
- search [ref=f4e158]:
- generic [ref=f4e159]: Asset 검색
- generic [ref=f4e160]:
- searchbox "Asset 검색" [ref=f4e161]
- button "검색" [ref=f4e162]
- generic [ref=f4e163]:
- checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f4e164]
- generic [ref=f4e165]: 삽입할 때 크게 보기 허용
- status [ref=f4e166]: 삽입할 수 있는 Asset 4개
- list [ref=f4e167]:
- listitem [ref=f4e168]:
- button "ap1-custody-v3-6e0376d2" [ref=f4e169]
- button "삭제" [ref=f4e170]
- listitem [ref=f4e171]:
- button "ap1-custody-v2-e110bd98" [ref=f4e172]
- button "삭제" [ref=f4e173]
- listitem [ref=f4e174]:
- button "ap1-credential-custody-f5e0c027" [ref=f4e175]
- button "삭제" [ref=f4e176]
- listitem [ref=f4e177]:
- button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f4e178]
- button "삭제" [ref=f4e179]
- dialog [ref=f4e194]:
- generic [ref=f4e195]:
- paragraph [ref=f4e196]: ASSET UPLOAD
- heading "Asset 업로드" [level=2] [ref=f4e197]
- paragraph [ref=f4e198]: 업로드한 파일은 서버 검증을 거친 뒤에만 본문에 삽입할 수 있습니다. 장식용이 아니면 대체 텍스트가 필요합니다.
- generic [ref=f4e199]:
- generic [ref=f4e200]: Asset 파일
- button "Asset 파일" [active] [ref=f4e201]
- generic [ref=f4e202]:
- checkbox "장식용 이미지 (대체 텍스트 없음)" [ref=f4e203]
- generic [ref=f4e204]: 장식용 이미지 (대체 텍스트 없음)
- generic [ref=f4e205]:
- generic [ref=f4e206]: 대체 텍스트
- textbox "대체 텍스트" [ref=f4e207]
- status "업로드 상태" [ref=f4e208]
- generic [ref=f4e209]:
- button "닫기" [ref=f4e210]
- button "업로드" [ref=f4e211]
- complementary [ref=f4e180]:
- paragraph [ref=f4e181]: WORKING COPY
- heading "작업 상태" [level=2] [ref=f4e182]
- status "편집 상태" [ref=f4e183]: 저장됨
- generic [ref=f4e184]:
- generic [ref=f4e185]:
- term [ref=f4e186]: 저장 버전
- definition [ref=f4e187]: "1"
- generic [ref=f4e188]:
- term [ref=f4e189]: 종류
- definition [ref=f4e190]: CASE
- button "저장" [disabled] [ref=f4e191]
- button "게시" [ref=f4e192]
- paragraph [ref=f4e193]: 불완전한 초안도 저장할 수 있습니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- paragraph [ref=f4e58]: Case 작업본을 만들었습니다.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
- generic [ref=f2e3]:
- link "본문으로 건너뛰기" [ref=f2e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f2e5]:
- generic [ref=f2e6]:
- link "TechLog Studio" [ref=f2e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f2e8]: Studio
- navigation "Studio 주 탐색" [ref=f2e10]:
- link "작업본" [ref=f2e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f2e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f2e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f2e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f2e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f2e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f2e17]
- main [ref=f2e18]:
- generic [ref=f2e19]:
- generic [ref=f2e20]:
- generic [ref=f2e21]:
- paragraph [ref=f2e22]: WORKSPACE
- heading "작업 흐름" [level=1] [ref=f2e23]
- paragraph [ref=f2e24]: 작성 중인 기록을 이어서 정리하고 검증·게시 흐름으로 연결합니다.
- link "새 문서" [ref=f2e25] [cursor=pointer]:
- /url: /studio/documents/new
- region "Studio 요약" [ref=f2e26]:
- generic [ref=f2e27]:
- generic [ref=f2e28]: 전체 작업본
- strong [ref=f2e29]: "2"
- generic [ref=f2e30]:
- generic [ref=f2e31]: 검증할 기록
- strong [ref=f2e32]: "2"
- generic [ref=f2e33]:
- generic [ref=f2e34]: 게시 준비
- strong [ref=f2e35]: "0"
- generic [ref=f2e36]:
- generic [ref=f2e37]: 게시 기록
- strong [ref=f2e38]: "1"
- generic [ref=f2e39]:
- generic [ref=f2e40]:
- heading "이어서 작성" [level=2] [ref=f2e41]
- link "전체 보기" [ref=f2e42] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f2e43]:
- article [ref=f2e44]:
- paragraph [ref=f2e45]: Case
- generic [ref=f2e46]:
- heading [level=3] [ref=f2e47]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f2e48] [cursor=pointer]:
- /url: /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit
- paragraph [ref=f2e49]: KeyCloak Patterns · 검증하기
- time [ref=f2e50]: 2026. 8. 24.
- article [ref=f2e51]:
- paragraph [ref=f2e52]: Case
- generic [ref=f2e53]:
- heading [level=3] [ref=f2e54]:
- link "Refresh Token만 서버로 옮겼지만 Access Token은 여전히 Browser에 남은 문제" [ref=f2e55] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f2e56]: KeyCloak Patterns · 검증하기
- time [ref=f2e57]: 2026. 8. 23.
- generic [ref=f2e58]:
- generic [ref=f2e59]:
- heading "검증과 미리보기" [level=2] [ref=f2e60]
- link "전체 보기" [ref=f2e61] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f2e62]:
- article [ref=f2e63]:
- paragraph [ref=f2e64]: Case
- generic [ref=f2e65]:
- heading [level=3] [ref=f2e66]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f2e67] [cursor=pointer]:
- /url: /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit
- paragraph [ref=f2e68]: KeyCloak Patterns · 검증하기
- time [ref=f2e69]: 2026. 8. 24.
- article [ref=f2e70]:
- paragraph [ref=f2e71]: Case
- generic [ref=f2e72]:
- heading [level=3] [ref=f2e73]:
- link "Refresh Token만 서버로 옮겼지만 Access Token은 여전히 Browser에 남은 문제" [ref=f2e74] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f2e75]: KeyCloak Patterns · 검증하기
- time [ref=f2e76]: 2026. 8. 23.
- generic [ref=f2e77]:
- generic [ref=f2e78]:
- heading "게시 준비" [level=2] [ref=f2e79]
- link "전체 보기" [ref=f2e80] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f2e82]: 게시 준비가 끝난 문서가 없습니다.
- region [ref=f2e83]:
- generic [ref=f2e84]:
- paragraph [ref=f2e85]: PUBLIC HOME
- heading "지금 집중하는 것" [level=2] [ref=f2e86]
- paragraph [ref=f2e87]: 공개 홈 맨 위 영역입니다. 셋 다 비워 두면 그 영역은 나타나지 않습니다.
- generic [ref=f2e88]:
- generic [ref=f2e89]:
- generic [ref=f2e90]:
- generic [ref=f2e91]: 현재 작업 (프로젝트)
- combobox "현재 작업 (프로젝트) 단계·현재 목표·다음 작업이 카드에 함께 나옵니다." [ref=f2e92]:
- option "고르지 않음"
- option "KeyCloak Patterns" [selected]
- option "Backend Clean Architecture (비공개)"
- option "Liner N + 1문제 (비공개)"
- generic [ref=f2e93]: 단계·현재 목표·다음 작업이 카드에 함께 나옵니다.
- generic [ref=f2e94]:
- generic [ref=f2e95]: 열린 질문
- combobox "열린 질문 아직 Question 문서가 없습니다." [disabled] [ref=f2e96]:
- option "고르지 않음" [selected]
- generic [ref=f2e97]: 아직 Question 문서가 없습니다.
- generic [ref=f2e98]:
- generic [ref=f2e99]: 최근 결정
- combobox "최근 결정 아직 Decision 문서가 없습니다." [disabled] [ref=f2e100]:
- option "고르지 않음" [selected]
- generic [ref=f2e101]: 아직 Decision 문서가 없습니다.
- generic [ref=f2e102]:
- button "홈 설정 저장" [ref=f2e103]
- paragraph [ref=f2e104]:
- text: 저장하면 공개 홈에 바로 반영됩니다.
- link "주제·프로젝트" [ref=f2e105] [cursor=pointer]:
- /url: /studio/taxonomy
- text: 에서 프로젝트를 만들고 게시할 수 있습니다.
- generic [ref=f2e106]:
- generic [ref=f2e107]:
- heading "최근 게시" [level=2] [ref=f2e108]
- link "게시 기록 보기" [ref=f2e109] [cursor=pointer]:
- /url: /studio/publications
- article [ref=f2e111]:
- paragraph [ref=f2e112]: 게시
- generic [ref=f2e113]:
- heading "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [level=3] [ref=f2e114]
- paragraph [ref=f2e115]: KeyCloak Patterns
- time [ref=f2e116]: 2026. 8. 23.
- paragraph [ref=f2e117]
@@ -0,0 +1,146 @@
- generic [ref=f6e3]:
- link "본문으로 건너뛰기" [ref=f6e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f6e5]:
- generic [ref=f6e6]:
- link "TechLog Studio" [ref=f6e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f6e8]: Studio
- navigation "Studio 주 탐색" [ref=f6e10]:
- link "작업본" [ref=f6e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f6e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f6e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f6e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f6e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f6e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f6e17]
- main [ref=f6e18]:
- generic [ref=f6e19]:
- generic [ref=f6e20]:
- generic [ref=f6e21]:
- paragraph [ref=f6e22]: WORKSPACE
- heading "작업 흐름" [level=1] [ref=f6e23]
- paragraph [ref=f6e24]: 작성 중인 기록을 이어서 정리하고 검증·게시 흐름으로 연결합니다.
- link "새 문서" [ref=f6e25] [cursor=pointer]:
- /url: /studio/documents/new
- region "Studio 요약" [ref=f6e26]:
- generic [ref=f6e27]:
- generic [ref=f6e28]: 전체 작업본
- strong [ref=f6e29]: "2"
- generic [ref=f6e30]:
- generic [ref=f6e31]: 검증할 기록
- strong [ref=f6e32]: "2"
- generic [ref=f6e33]:
- generic [ref=f6e34]: 게시 준비
- strong [ref=f6e35]: "0"
- generic [ref=f6e36]:
- generic [ref=f6e37]: 게시 기록
- strong [ref=f6e38]: "2"
- generic [ref=f6e39]:
- generic [ref=f6e40]:
- heading "이어서 작성" [level=2] [ref=f6e41]
- link "전체 보기" [ref=f6e42] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f6e43]:
- article [ref=f6e44]:
- paragraph [ref=f6e45]: Case
- generic [ref=f6e46]:
- heading [level=3] [ref=f6e47]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f6e48] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f6e49]: KeyCloak Patterns · 검증하기
- time [ref=f6e50]: 2026. 8. 24.
- article [ref=f6e51]:
- paragraph [ref=f6e52]: Case
- generic [ref=f6e53]:
- heading [level=3] [ref=f6e54]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f6e55] [cursor=pointer]:
- /url: /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit
- paragraph [ref=f6e56]: KeyCloak Patterns · 검증하기
- time [ref=f6e57]: 2026. 8. 24.
- generic [ref=f6e58]:
- generic [ref=f6e59]:
- heading "검증과 미리보기" [level=2] [ref=f6e60]
- link "전체 보기" [ref=f6e61] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f6e62]:
- article [ref=f6e63]:
- paragraph [ref=f6e64]: Case
- generic [ref=f6e65]:
- heading [level=3] [ref=f6e66]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f6e67] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f6e68]: KeyCloak Patterns · 검증하기
- time [ref=f6e69]: 2026. 8. 24.
- article [ref=f6e70]:
- paragraph [ref=f6e71]: Case
- generic [ref=f6e72]:
- heading [level=3] [ref=f6e73]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f6e74] [cursor=pointer]:
- /url: /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit
- paragraph [ref=f6e75]: KeyCloak Patterns · 검증하기
- time [ref=f6e76]: 2026. 8. 24.
- generic [ref=f6e77]:
- generic [ref=f6e78]:
- heading "게시 준비" [level=2] [ref=f6e79]
- link "전체 보기" [ref=f6e80] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f6e82]: 게시 준비가 끝난 문서가 없습니다.
- region [ref=f6e83]:
- generic [ref=f6e84]:
- paragraph [ref=f6e85]: PUBLIC HOME
- heading "지금 집중하는 것" [level=2] [ref=f6e86]
- paragraph [ref=f6e87]: 공개 홈 맨 위 영역입니다. 셋 다 비워 두면 그 영역은 나타나지 않습니다.
- generic [ref=f6e88]:
- generic [ref=f6e89]:
- generic [ref=f6e90]:
- generic [ref=f6e91]: 현재 작업 (프로젝트)
- combobox "현재 작업 (프로젝트) 단계·현재 목표·다음 작업이 카드에 함께 나옵니다." [ref=f6e92]:
- option "고르지 않음"
- option "KeyCloak Patterns" [selected]
- option "Backend Clean Architecture (비공개)"
- option "Liner N + 1문제 (비공개)"
- generic [ref=f6e93]: 단계·현재 목표·다음 작업이 카드에 함께 나옵니다.
- generic [ref=f6e94]:
- generic [ref=f6e95]: 열린 질문
- combobox "열린 질문 아직 Question 문서가 없습니다." [disabled] [ref=f6e96]:
- option "고르지 않음" [selected]
- generic [ref=f6e97]: 아직 Question 문서가 없습니다.
- generic [ref=f6e98]:
- generic [ref=f6e99]: 최근 결정
- combobox "최근 결정 아직 Decision 문서가 없습니다." [disabled] [ref=f6e100]:
- option "고르지 않음" [selected]
- generic [ref=f6e101]: 아직 Decision 문서가 없습니다.
- generic [ref=f6e102]:
- button "홈 설정 저장" [ref=f6e103]
- paragraph [ref=f6e104]:
- text: 저장하면 공개 홈에 바로 반영됩니다.
- link "주제·프로젝트" [ref=f6e105] [cursor=pointer]:
- /url: /studio/taxonomy
- text: 에서 프로젝트를 만들고 게시할 수 있습니다.
- generic [ref=f6e106]:
- generic [ref=f6e107]:
- heading "최근 게시" [level=2] [ref=f6e108]
- link "게시 기록 보기" [ref=f6e109] [cursor=pointer]:
- /url: /studio/publications
- generic [ref=f6e110]:
- article [ref=f6e111]:
- paragraph [ref=f6e112]: 게시
- generic [ref=f6e113]:
- heading "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [level=3] [ref=f6e114]
- paragraph [ref=f6e115]: KeyCloak Patterns
- time [ref=f6e116]: 2026. 8. 24.
- article [ref=f6e117]:
- paragraph [ref=f6e118]: 게시
- generic [ref=f6e119]:
- heading "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [level=3] [ref=f6e120]
- paragraph [ref=f6e121]: KeyCloak Patterns
- time [ref=f6e122]: 2026. 8. 23.
- paragraph [ref=f6e123]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,259 @@
- generic [ref=f4e3]:
- link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f4e5]:
- generic [ref=f4e6]:
- link "TechLog Studio" [ref=f4e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f4e8]: Studio
- navigation "Studio 주 탐색" [ref=f4e10]:
- link "작업본" [ref=f4e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f4e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f4e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f4e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f4e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f4e17]
- main [ref=f4e18]:
- generic [ref=f4e19]:
- generic [ref=f4e20]:
- region [ref=f4e21]:
- generic [ref=f4e22]:
- paragraph [ref=f4e23]: PROJECT_DECISION · VERSION 10
- heading "문서 편집" [level=1] [ref=f4e24]
- paragraph [ref=f4e25]: 외부 IdP Federation을 별도의 인증 구조로 세지 않는다
- region [ref=f4e26]:
- generic [ref=f4e27]:
- paragraph [ref=f4e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f4e29]
- generic [ref=f4e30]:
- generic [ref=f4e31]:
- generic [ref=f4e32]: 제목
- textbox "제목" [ref=f4e33]: 외부 IdP Federation을 별도의 인증 구조로 세지 않는다
- generic [ref=f4e34]:
- generic [ref=f4e35]: slug
- textbox "slug" [ref=f4e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: federation-is-not-an-application-pattern
- generic [ref=f4e37]:
- generic [ref=f4e38]: 요약
- textbox "요약" [ref=f4e39]: Google은 upstream IdP, Keycloak은 애플리케이션이 신뢰하는 issuer이자 broker, 네 구조는 애플리케이션 credential 경계다. 세 층을 분리해서 적고 소셜 로그인 추가를 인증 구조 변경으로 세지 않는다.
- generic [ref=f4e40]:
- generic [ref=f4e41]: Topic
- combobox "Topic" [ref=f4e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f4e43]:
- generic [ref=f4e44]: Project
- combobox "Project" [ref=f4e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "근거 기록" [ref=f4e46]:
- generic [ref=f4e48]:
- generic [ref=f4e49]:
- generic [ref=f4e50]: 근거 1 대상
- combobox "근거 1 대상" [ref=f4e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계" [selected]
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f4e52]:
- generic [ref=f4e53]: 근거 1 이유
- textbox "근거 1 이유" [ref=f4e54]: 이 결정을 규칙으로 편 기준이다.
- generic [ref=f4e55]:
- button "위로" [disabled] [ref=f4e56]
- button "아래로" [ref=f4e57]
- button "삭제" [ref=f4e58]
- generic [ref=f4e59]:
- generic [ref=f4e60]:
- generic [ref=f4e61]: 근거 2 대상
- combobox "근거 2 대상" [ref=f4e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계" [disabled]
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f4e63]:
- generic [ref=f4e64]: 근거 2 이유
- textbox "근거 2 이유" [ref=f4e65]: 브로커가 발급한 code를 받는 애플리케이션 경계다.
- generic [ref=f4e66]:
- button "위로" [ref=f4e67]
- button "아래로" [ref=f4e68]
- button "삭제" [ref=f4e69]
- generic [ref=f4e70]:
- generic [ref=f4e71]:
- generic [ref=f4e72]: 근거 3 대상
- combobox "근거 3 대상" [ref=f4e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계" [disabled]
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [selected]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f4e74]:
- generic [ref=f4e75]: 근거 3 이유
- textbox "근거 3 이유" [ref=f4e76]: upstream IdP 상태와 애플리케이션 상태를 같은 이름으로 부르지 않는다.
- generic [ref=f4e77]:
- button "위로" [ref=f4e78]
- button "아래로" [disabled] [ref=f4e79]
- button "삭제" [ref=f4e80]
- button "근거 추가" [ref=f4e81]
- region [ref=f4e82]:
- generic [ref=f4e83]:
- paragraph [ref=f4e84]: PROJECT DECISION
- heading "프로젝트 결정" [level=2] [ref=f4e85]
- generic [ref=f4e86]:
- generic [ref=f4e87]:
- generic [ref=f4e88]: 결정 상태
- combobox "결정 상태" [ref=f4e89]:
- option "아직 정하지 않음"
- option "PROPOSED"
- option "ADOPTED" [selected]
- generic [ref=f4e90]:
- generic [ref=f4e91]: 결정일
- textbox "결정일" [ref=f4e92]: 2026-08-24
- generic [ref=f4e93]:
- generic [ref=f4e94]: 결정문
- textbox "결정문" [ref=f4e95]: 외부 IdP federation을 다섯 번째 인증 구조로 세지 않는다. Google은 upstream IdP, Keycloak은 애플리케이션이 신뢰하는 issuer이자 broker, 네 구조는 애플리케이션 credential 경계로 각각 분리해 적는다.
- generic [ref=f4e96]:
- generic [ref=f4e97]: 판단 이유
- textbox "판단 이유" [ref=f4e98]: Google을 구조 하나로 세게 되면 upstream IdP 경계와 애플리케이션 OAuth 경계를 같은 기준으로 묶게 되는데, 두 경계는 검증 방법이 서로 다르다. 사용자가 Keycloak 로그인 화면에서 Google을 고르면 브라우저가 Google authorization endpoint로 이동한다. Keycloak은 Google의 응답을 검증해 local identity와 연결한 뒤 자기 authorization code를 애플리케이션 callback으로 보낸다. 이후 애플리케이션은 Google이 아니라 Keycloak을 상대로 code를 token으로 교환한다. Resource Server가 검증하는 issuer도 브로커이고 애플리케이션은 Google token을 받지 않기 때문에, 소셜 로그인을 붙여도 브라우저가 token을 받는지와 어느 계층이 API를 부르는지는 하나도 바뀌지 않는다. 두 경계를 섞어 두게 되면 비교표에 성격이 다른 항목이 끼어들고, 계정 연결 규칙도 인증 구조 이야기에 섞여서 따로 설계하지 않고 넘어가게 된다.
- group "영향" [ref=f4e99]:
- generic [ref=f4e101]:
- generic [ref=f4e102]:
- generic [ref=f4e103]: 영향 1
- textbox "영향 1" [ref=f4e104]: Google을 추가해도 애플리케이션이 검증하는 issuer는 Keycloak으로 유지한다. 네 구조의 credential 배치 기준은 바뀌지 않는다.
- generic [ref=f4e105]:
- button "위로" [disabled] [ref=f4e106]
- button "아래로" [ref=f4e107]
- button "삭제" [ref=f4e108]
- generic [ref=f4e109]:
- generic [ref=f4e110]:
- generic [ref=f4e111]: 영향 2
- textbox "영향 2" [ref=f4e112]: 계정 연결을 별도 문제로 다뤄야 하고, provider와 upstream subject의 조합을 열쇠로 쓰면서 email이 같다고 자동 병합하지 않는다.
- generic [ref=f4e113]:
- button "위로" [ref=f4e114]
- button "아래로" [ref=f4e115]
- button "삭제" [ref=f4e116]
- generic [ref=f4e117]:
- generic [ref=f4e118]:
- generic [ref=f4e119]: 영향 3
- textbox "영향 3" [ref=f4e120]: 검증 범위를 두 겹으로 적어야 해서 mock provider로 확인한 broker·claim mapping 계약과 실제 계정·공개 HTTPS callback·consent를 구분하게 된다.
- generic [ref=f4e121]:
- button "위로" [ref=f4e122]
- button "아래로" [ref=f4e123]
- button "삭제" [ref=f4e124]
- generic [ref=f4e125]:
- generic [ref=f4e126]:
- generic [ref=f4e127]: 영향 4
- textbox "영향 4" [ref=f4e128]: upstream IdP가 늘면 브로커 설정이 늘어나게 되어서 그 설정의 소유자를 애플리케이션 팀과 따로 정해야 한다.
- generic [ref=f4e129]:
- button "위로" [ref=f4e130]
- button "아래로" [disabled] [ref=f4e131]
- button "삭제" [ref=f4e132]
- button "영향 추가" [ref=f4e133]
- region [ref=f4e134]:
- generic [ref=f4e135]:
- paragraph [ref=f4e136]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f4e137]
- generic [ref=f4e140]:
- navigation "문서 경로" [ref=f4e141]:
- link "Project" [ref=f4e142] [cursor=pointer]:
- /url: /projects
- generic [ref=f4e143]: /
- link "KeyCloak Patterns" [ref=f4e144] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- generic [ref=f4e145]: /
- link "Decision" [ref=f4e146] [cursor=pointer]:
- /url: /projects/keycloak-patterns/decisions
- list [ref=f4e147]:
- listitem [ref=f4e148]:
- article [ref=f4e149]:
- generic [ref=f4e150]:
- generic [ref=f4e151]:
- generic [ref=f4e152]: ADOPTED
- time [ref=f4e153]: 2026.08.24
- heading "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" [level=2] [ref=f4e154]
- paragraph [ref=f4e155]: 외부 IdP federation을 다섯 번째 인증 구조로 세지 않는다.Google은 upstream IdP, Keycloak은 애플리케이션이 신뢰하는 issuer이자 broker, 네 구조는 애플리케이션 credential 경계로 각각 분리해 적는다.
- generic [ref=f4e156]:
- heading "판단 이유" [level=3] [ref=f4e157]
- paragraph [ref=f4e158]: Google을 구조 하나로 세게 되면 upstream IdP 경계와 애플리케이션 OAuth 경계를 같은 기준으로 묶게 되는데, 두 경계는 검증 방법이 서로 다르다.사용자가 Keycloak 로그인 화면에서 Google을 고르면 브라우저가 Google authorization endpoint로 이동한다. Keycloak은 Google의 응답을 검증해 local identity와 연결한 뒤 자기 authorization code를 애플리케이션 callback으로 보낸다. 이후 애플리케이션은 Google이 아니라 Keycloak을 상대로 code를 token으로 교환한다.Resource Server가 검증하는 issuer도 브로커이고 애플리케이션은 Google token을 받지 않기 때문에, 소셜 로그인을 붙여도 브라우저가 token을 받는지와 어느 계층이 API를 부르는지는 하나도 바뀌지 않는다.두 경계를 섞어 두게 되면 비교표에 성격이 다른 항목이 끼어들고, 계정 연결 규칙도 인증 구조 이야기에 섞여서 따로 설계하지 않고 넘어가게 된다.
- generic [ref=f4e159]:
- heading "영향" [level=3] [ref=f4e160]
- list [ref=f4e161]:
- listitem [ref=f4e162]: Google을 추가해도 애플리케이션이 검증하는 issuer는 Keycloak으로 유지한다. 네 구조의 credential 배치 기준은 바뀌지 않는다.
- listitem [ref=f4e163]: 계정 연결을 별도 문제로 다뤄야 하고, provider와 upstream subject의 조합을 열쇠로 쓰면서 email이 같다고 자동 병합하지 않는다.
- listitem [ref=f4e164]: 검증 범위를 두 겹으로 적어야 해서 mock provider로 확인한 broker·claim mapping 계약과 실제 계정·공개 HTTPS callback·consent를 구분하게 된다.
- listitem [ref=f4e165]: upstream IdP가 늘면 브로커 설정이 늘어나게 되어서 그 설정의 소유자를 애플리케이션 팀과 따로 정해야 한다.
- generic [ref=f4e166]:
- heading "근거 기록" [level=3] [ref=f4e167]
- list [ref=f4e168]:
- listitem [ref=f4e169]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f4e170] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- complementary [ref=f4e171]:
- heading "작업 상태" [level=2] [ref=f4e172]
- status "편집 상태" [ref=f4e173]: 저장됨
- generic [ref=f4e174]:
- generic [ref=f4e175]:
- term [ref=f4e176]: 저장 버전
- definition [ref=f4e177]: "10"
- generic [ref=f4e178]:
- term [ref=f4e179]: 종류
- definition [ref=f4e180]: Decision
- paragraph [ref=f4e181]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f4e182]:
- button "저장" [disabled] [ref=f4e183]
- button "게시" [ref=f4e184]
- paragraph [ref=f4e185]: 버전 10으로 저장했습니다.
@@ -0,0 +1,319 @@
- generic [ref=f5e3]:
- link "본문으로 건너뛰기" [ref=f5e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f5e5]:
- generic [ref=f5e6]:
- link "TechLog Studio" [ref=f5e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f5e8]: Studio
- navigation "Studio 주 탐색" [ref=f5e10]:
- link "작업본" [ref=f5e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f5e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f5e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f5e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f5e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f5e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f5e17]
- main [ref=f5e18]:
- generic [ref=f5e19]:
- generic [ref=f5e20]:
- region [ref=f5e21]:
- generic [ref=f5e22]:
- paragraph [ref=f5e23]: PROJECT_DECISION · VERSION 12
- heading "문서 편집" [level=1] [ref=f5e24]
- paragraph [ref=f5e25]: 인증 구조를 보안 성숙도 단계로 취급하지 않는다
- region [ref=f5e26]:
- generic [ref=f5e27]:
- paragraph [ref=f5e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f5e29]
- generic [ref=f5e30]:
- generic [ref=f5e31]:
- generic [ref=f5e32]: 제목
- textbox "제목" [ref=f5e33]: 인증 구조를 보안 성숙도 단계로 취급하지 않는다
- generic [ref=f5e34]:
- generic [ref=f5e35]: slug
- textbox "slug" [ref=f5e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: patterns-are-not-a-maturity-ladder
- generic [ref=f5e37]:
- generic [ref=f5e38]: 요약
- textbox "요약" [ref=f5e39]: SPA, Mediator, BFF, Forward-Auth는 credential을 처리하는 주체와 API 호출 경로가 서로 다르다. 번호나 브라우저 token 노출 여부를 보안 등급으로 사용하지 않고 각각 별도의 아키텍처 패턴으로 취급한다.
- generic [ref=f5e40]:
- generic [ref=f5e41]: Topic
- combobox "Topic" [ref=f5e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f5e43]:
- generic [ref=f5e44]: Project
- combobox "Project" [ref=f5e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "근거 기록" [ref=f5e46]:
- generic [ref=f5e48]:
- generic [ref=f5e49]:
- generic [ref=f5e50]: 근거 1 대상
- combobox "근거 1 대상" [ref=f5e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f5e52]:
- generic [ref=f5e53]: 근거 1 이유
- textbox "근거 1 이유" [ref=f5e54]: 브라우저가 code 교환, token 보관, API 호출을 직접 수행한다.
- generic [ref=f5e55]:
- button "위로" [disabled] [ref=f5e56]
- button "아래로" [ref=f5e57]
- button "삭제" [ref=f5e58]
- generic [ref=f5e59]:
- generic [ref=f5e60]:
- generic [ref=f5e61]: 근거 2 대상
- combobox "근거 2 대상" [ref=f5e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f5e63]:
- generic [ref=f5e64]: 근거 2 이유
- textbox "근거 2 이유" [ref=f5e65]: mediator가 code 교환과 refresh token 보관을 담당하고 브라우저가 access token으로 API를 직접 호출한다.
- generic [ref=f5e66]:
- button "위로" [ref=f5e67]
- button "아래로" [ref=f5e68]
- button "삭제" [ref=f5e69]
- generic [ref=f5e70]:
- generic [ref=f5e71]:
- generic [ref=f5e72]: 근거 3 대상
- combobox "근거 3 대상" [ref=f5e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f5e74]:
- generic [ref=f5e75]: 근거 3 이유
- textbox "근거 3 이유" [ref=f5e76]: BFF가 token과 session을 server-side에서 관리하고 Resource Server를 호출한다.
- generic [ref=f5e77]:
- button "위로" [ref=f5e78]
- button "아래로" [ref=f5e79]
- button "삭제" [ref=f5e80]
- generic [ref=f5e81]:
- generic [ref=f5e82]:
- generic [ref=f5e83]: 근거 4 대상
- combobox "근거 4 대상" [ref=f5e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f5e85]:
- generic [ref=f5e86]: 근거 4 이유
- textbox "근거 4 이유" [ref=f5e87]: oauth2-proxy가 인증을 처리하고 upstream에는 identity header를 전달한다.
- generic [ref=f5e88]:
- button "위로" [ref=f5e89]
- button "아래로" [ref=f5e90]
- button "삭제" [ref=f5e91]
- generic [ref=f5e92]:
- generic [ref=f5e93]:
- generic [ref=f5e94]: 근거 5 대상
- combobox "근거 5 대상" [ref=f5e95]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [selected]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f5e96]:
- generic [ref=f5e97]: 근거 5 이유
- textbox "근거 5 이유" [ref=f5e98]: 이 결정을 적용하는 선택 기준이다.
- generic [ref=f5e99]:
- button "위로" [ref=f5e100]
- button "아래로" [disabled] [ref=f5e101]
- button "삭제" [ref=f5e102]
- button "근거 추가" [ref=f5e103]
- region [ref=f5e104]:
- generic [ref=f5e105]:
- paragraph [ref=f5e106]: PROJECT DECISION
- heading "프로젝트 결정" [level=2] [ref=f5e107]
- generic [ref=f5e108]:
- generic [ref=f5e109]:
- generic [ref=f5e110]: 결정 상태
- combobox "결정 상태" [ref=f5e111]:
- option "아직 정하지 않음"
- option "PROPOSED"
- option "ADOPTED" [selected]
- generic [ref=f5e112]:
- generic [ref=f5e113]: 결정일
- textbox "결정일" [ref=f5e114]: 2026-08-24
- generic [ref=f5e115]:
- generic [ref=f5e116]: 결정문
- textbox "결정문" [ref=f5e117]: SPA에서 Mediator, BFF, OAuth2-Proxy로 가는 순서를 낮은 보안에서 높은 보안으로 가는 단계로 모델링하지 않는다. 네 구조는 credential과 인증 상태를 처리하는 주체가 서로 다른 별개의 아키텍처 패턴으로 취급한다.
- generic [ref=f5e118]:
- generic [ref=f5e119]: 판단 이유
- textbox "판단 이유" [ref=f5e120]: BFF는 브라우저 token을 없애지만 server session과 CSRF, 공유 저장소를 만든다. Forward-Auth는 애플리케이션의 token custody를 줄이지만 edge 헤더 신뢰와 network 경계를 만든다. 뒤 구조가 앞 구조의 문제를 없애는 것이 아니라 다른 곳에 다른 요구를 만든다. 네 구조의 차이는 code를 교환하는 주체, token 저장 방식, API 호출 주체, 보호 자원이 신뢰하는 credential에서 확인됐다. 이 차이를 보안 성숙도 순서로 환산하지 않는다. 성숙도 모델로 두면 「일단 제일 뒤 구조로 가자」는 판단이 나온다. backend 직접 경로를 닫을 수 없는 환경에서 edge에 인증을 맡기면 upstream이 헤더 하나로 사용자를 판단하는데 그 헤더를 누구나 만들어 보낼 수 있다. 그런 환경에서는 브라우저가 token을 직접 들고 서명을 검증받는 구조가 낫다.
- group "영향" [ref=f5e121]:
- generic [ref=f5e123]:
- generic [ref=f5e124]:
- generic [ref=f5e125]: 영향 1
- textbox "영향 1" [ref=f5e126]: 패턴을 비교할 때는 적용 조건과 운영해야 할 상태, 신뢰 경계, 장애 지점을 함께 적는다. 브라우저 token 노출 여부 하나만으로 순서를 매기지 않는다.
- generic [ref=f5e127]:
- button "위로" [disabled] [ref=f5e128]
- button "아래로" [ref=f5e129]
- button "삭제" [ref=f5e130]
- generic [ref=f5e131]:
- generic [ref=f5e132]:
- generic [ref=f5e133]: 영향 2
- textbox "영향 2" [ref=f5e134]: 구조를 고를 때 번호가 아니라 code 교환·token 보관·API 호출의 배치를 먼저 답한다. 뒤 구조에서 앞 구조로 되돌아가는 선택도 후퇴가 아니라 credential 계약의 변경으로 적는다.
- generic [ref=f5e135]:
- button "위로" [ref=f5e136]
- button "아래로" [ref=f5e137]
- button "삭제" [ref=f5e138]
- generic [ref=f5e139]:
- generic [ref=f5e140]:
- generic [ref=f5e141]: 영향 3
- textbox "영향 3" [ref=f5e142]: 구조 이름만으로 운영 속성을 추정하지 않는다. 공유 저장소와 장애 복구, secret 교체는 매번 따로 확인한다.
- generic [ref=f5e143]:
- button "위로" [ref=f5e144]
- button "아래로" [disabled] [ref=f5e145]
- button "삭제" [ref=f5e146]
- button "영향 추가" [ref=f5e147]
- region [ref=f5e148]:
- generic [ref=f5e149]:
- paragraph [ref=f5e150]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f5e151]
- generic [ref=f5e154]:
- navigation "문서 경로" [ref=f5e155]:
- link "Project" [ref=f5e156] [cursor=pointer]:
- /url: /projects
- generic [ref=f5e157]: /
- link "KeyCloak Patterns" [ref=f5e158] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- generic [ref=f5e159]: /
- link "Decision" [ref=f5e160] [cursor=pointer]:
- /url: /projects/keycloak-patterns/decisions
- list [ref=f5e161]:
- listitem [ref=f5e162]:
- article [ref=f5e163]:
- generic [ref=f5e164]:
- generic [ref=f5e165]:
- generic [ref=f5e166]: ADOPTED
- time [ref=f5e167]: 2026.08.24
- heading "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [level=2] [ref=f5e168]
- paragraph [ref=f5e169]: SPA에서 Mediator, BFF, OAuth2-Proxy로 가는 순서를 낮은 보안에서 높은 보안으로 가는 단계로 모델링하지 않는다.네 구조는 credential과 인증 상태를 처리하는 주체가 서로 다른 별개의 아키텍처 패턴으로 취급한다.
- generic [ref=f5e170]:
- heading "판단 이유" [level=3] [ref=f5e171]
- paragraph [ref=f5e172]: BFF는 브라우저 token을 없애지만 server session과 CSRF, 공유 저장소를 만든다. Forward-Auth는 애플리케이션의 token custody를 줄이지만 edge 헤더 신뢰와 network 경계를 만든다. 뒤 구조가 앞 구조의 문제를 없애는 것이 아니라 다른 곳에 다른 요구를 만든다.네 구조의 차이는 code를 교환하는 주체, token 저장 방식, API 호출 주체, 보호 자원이 신뢰하는 credential에서 확인됐다. 이 차이를 보안 성숙도 순서로 환산하지 않는다.성숙도 모델로 두면 「일단 제일 뒤 구조로 가자」는 판단이 나온다. backend 직접 경로를 닫을 수 없는 환경에서 edge에 인증을 맡기면 upstream이 헤더 하나로 사용자를 판단하는데 그 헤더를 누구나 만들어 보낼 수 있다. 그런 환경에서는 브라우저가 token을 직접 들고 서명을 검증받는 구조가 낫다.
- generic [ref=f5e173]:
- heading "영향" [level=3] [ref=f5e174]
- list [ref=f5e175]:
- listitem [ref=f5e176]: 패턴을 비교할 때는 적용 조건과 운영해야 할 상태, 신뢰 경계, 장애 지점을 함께 적는다. 브라우저 token 노출 여부 하나만으로 순서를 매기지 않는다.
- listitem [ref=f5e177]: 구조를 고를 때 번호가 아니라 code 교환·token 보관·API 호출의 배치를 먼저 답한다. 뒤 구조에서 앞 구조로 되돌아가는 선택도 후퇴가 아니라 credential 계약의 변경으로 적는다.
- listitem [ref=f5e178]: 구조 이름만으로 운영 속성을 추정하지 않는다. 공유 저장소와 장애 복구, secret 교체는 매번 따로 확인한다.
- generic [ref=f5e179]:
- heading "근거 기록" [level=3] [ref=f5e180]
- list [ref=f5e181]:
- listitem [ref=f5e182]:
- link "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f5e183] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- listitem [ref=f5e184]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f5e185] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- listitem [ref=f5e186]:
- link "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f5e187] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- listitem [ref=f5e188]:
- link "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f5e189] [cursor=pointer]:
- /url: /cases/identity-header-trust
- complementary [ref=f5e190]:
- heading "작업 상태" [level=2] [ref=f5e191]
- status "편집 상태" [ref=f5e192]: 저장됨
- generic [ref=f5e193]:
- generic [ref=f5e194]:
- term [ref=f5e195]: 저장 버전
- definition [ref=f5e196]: "12"
- generic [ref=f5e197]:
- term [ref=f5e198]: 종류
- definition [ref=f5e199]: Decision
- paragraph [ref=f5e200]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f5e201]:
- button "저장" [disabled] [ref=f5e202]
- button "게시" [ref=f5e203]
- paragraph [ref=f5e204]: 버전 12으로 저장했습니다.
@@ -0,0 +1,319 @@
- generic [ref=f6e3]:
- link "본문으로 건너뛰기" [ref=f6e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f6e5]:
- generic [ref=f6e6]:
- link "TechLog Studio" [ref=f6e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f6e8]: Studio
- navigation "Studio 주 탐색" [ref=f6e10]:
- link "작업본" [ref=f6e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f6e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f6e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f6e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f6e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f6e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f6e17]
- main [ref=f6e18]:
- generic [ref=f6e19]:
- generic [ref=f6e20]:
- region [ref=f6e21]:
- generic [ref=f6e22]:
- paragraph [ref=f6e23]: PROJECT_DECISION · VERSION 12
- heading "문서 편집" [level=1] [ref=f6e24]
- paragraph [ref=f6e25]: BFF가 OAuth Token을 관리하는 조건
- region [ref=f6e26]:
- generic [ref=f6e27]:
- paragraph [ref=f6e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f6e29]
- generic [ref=f6e30]:
- generic [ref=f6e31]:
- generic [ref=f6e32]: 제목
- textbox "제목" [ref=f6e33]: BFF가 OAuth Token을 관리하는 조건
- generic [ref=f6e34]:
- generic [ref=f6e35]: slug
- textbox "slug" [ref=f6e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: bff-owns-token-when-browser-must-not
- generic [ref=f6e37]:
- generic [ref=f6e38]: 요약
- textbox "요약" [ref=f6e39]: "애플리케이션이 API 조합과 인가를 직접 처리하면서 브라우저에는 OAuth token을 전달하지 않아야 한다면 BFF가 authorization code 교환, token 보관, downstream 호출을 담당한다. 이 결정은 아직 프로젝트 기본값으로 채택하지 않아 `PROPOSED` 상태로 둔다."
- generic [ref=f6e40]:
- generic [ref=f6e41]: Topic
- combobox "Topic" [ref=f6e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f6e43]:
- generic [ref=f6e44]: Project
- combobox "Project" [ref=f6e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "근거 기록" [ref=f6e46]:
- generic [ref=f6e48]:
- generic [ref=f6e49]:
- generic [ref=f6e50]: 근거 1 대상
- combobox "근거 1 대상" [ref=f6e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f6e52]:
- generic [ref=f6e53]: 근거 1 이유
- textbox "근거 1 이유" [ref=f6e54]: 이 결정이 가리키는 구조를 실제로 실행해 본 기록이다.
- generic [ref=f6e55]:
- button "위로" [disabled] [ref=f6e56]
- button "아래로" [ref=f6e57]
- button "삭제" [ref=f6e58]
- generic [ref=f6e59]:
- generic [ref=f6e60]:
- generic [ref=f6e61]: 근거 2 대상
- combobox "근거 2 대상" [ref=f6e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f6e63]:
- generic [ref=f6e64]: 근거 2 이유
- textbox "근거 2 이유" [ref=f6e65]: 이 결정이 PROPOSED인 동안의 실제 적용 기준이다.
- generic [ref=f6e66]:
- button "위로" [ref=f6e67]
- button "아래로" [ref=f6e68]
- button "삭제" [ref=f6e69]
- generic [ref=f6e70]:
- generic [ref=f6e71]:
- generic [ref=f6e72]: 근거 3 대상
- combobox "근거 3 대상" [ref=f6e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [selected]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f6e74]:
- generic [ref=f6e75]: 근거 3 이유
- textbox "근거 3 이유" [ref=f6e76]: 이 결정을 적용할 조건과 피해야 할 조건이 여기 있다.
- generic [ref=f6e77]:
- button "위로" [ref=f6e78]
- button "아래로" [ref=f6e79]
- button "삭제" [ref=f6e80]
- generic [ref=f6e81]:
- generic [ref=f6e82]:
- generic [ref=f6e83]: 근거 4 대상
- combobox "근거 4 대상" [ref=f6e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser에서 OAuth Token을 제거해야 하는 경우 BFF가 Token을 소유한다"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준" [disabled]
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f6e85]:
- generic [ref=f6e86]: 근거 4 이유
- textbox "근거 4 이유" [ref=f6e87]: access token이 브라우저로 나가 이 요구를 만족하지 못한 경우다.
- generic [ref=f6e88]:
- button "위로" [ref=f6e89]
- button "아래로" [disabled] [ref=f6e90]
- button "삭제" [ref=f6e91]
- button "근거 추가" [ref=f6e92]
- region [ref=f6e93]:
- generic [ref=f6e94]:
- paragraph [ref=f6e95]: PROJECT DECISION
- heading "프로젝트 결정" [level=2] [ref=f6e96]
- generic [ref=f6e97]:
- generic [ref=f6e98]:
- generic [ref=f6e99]: 결정 상태
- combobox "결정 상태" [ref=f6e100]:
- option "아직 정하지 않음"
- option "PROPOSED" [selected]
- option "ADOPTED"
- generic [ref=f6e101]:
- generic [ref=f6e102]: 결정일
- textbox "결정일" [ref=f6e103]
- generic [ref=f6e104]:
- generic [ref=f6e105]: 결정문
- textbox "결정문" [ref=f6e106]: 브라우저에 OAuth token을 노출하지 않으면서 애플리케이션이 Resource Server 호출을 중계하고 조합해야 하는 경우, BFF가 authorization code 교환과 token 보관, downstream API 호출을 소유한다. 브라우저에는 애플리케이션 session만 제공한다.
- generic [ref=f6e107]:
- generic [ref=f6e108]: 판단 이유
- textbox "판단 이유" [ref=f6e109]: "브라우저에 OAuth token을 전달하지 않으려면 server가 authorization code를 교환하고 access token을 사용해 downstream API를 호출해야 한다. Mediator 구조에서는 브라우저가 Resource Server를 직접 호출하므로 access token을 `/token/access` 응답으로 전달한다. 따라서 브라우저에 OAuth token을 제공하지 않는다는 요구에는 맞지 않는다. Forward-Auth 구조도 브라우저에 OAuth token을 전달하지 않을 수 있지만 upstream은 JWT를 직접 검증하지 않고 edge가 제공한 identity header를 사용한다. 애플리케이션이 access token으로 여러 Resource Server를 직접 호출하거나 사용자별 API 조합을 처리해야 한다면 BFF 쪽이 요구에 더 잘 맞는다. 따라서 브라우저에 OAuth token을 전달하지 않는 조건만으로 BFF를 선택하지는 않는다. 애플리케이션이 downstream API 호출과 조합을 직접 맡아야 하는지도 함께 본다. 다만 상태를 ADOPTED로 올리지는 않는다. 지금 자료는 네 구조를 나란히 실행한 비교 실험이고 이 프로젝트가 BFF를 기본값으로 고른 기록이 없기 때문이다. 기본값으로 고른 시점과 그 근거가 생기면 그때 올리게 되고, 그 전까지 실제 적용 기준은 「BFF 인증 구조 설계 기준」 Reference다."
- group "영향" [ref=f6e110]:
- generic [ref=f6e112]:
- generic [ref=f6e113]:
- generic [ref=f6e114]: 영향 1
- textbox "영향 1" [ref=f6e115]: BFF가 로그인 상태와 token을 가진 보안 구성요소가 되어서 단순 proxy로 취급할 수 없게 된다.
- generic [ref=f6e116]:
- button "위로" [disabled] [ref=f6e117]
- button "아래로" [ref=f6e118]
- button "삭제" [ref=f6e119]
- generic [ref=f6e120]:
- generic [ref=f6e121]:
- generic [ref=f6e122]: 영향 2
- textbox "영향 2" [ref=f6e123]: 상태 변경 요청마다 CSRF 검증이 필요해지고, 노출 값과 제출 값이 다를 수 있어서 클라이언트 코드도 그 구분을 알아야 한다.
- generic [ref=f6e124]:
- button "위로" [ref=f6e125]
- button "아래로" [ref=f6e126]
- button "삭제" [ref=f6e127]
- generic [ref=f6e128]:
- generic [ref=f6e129]:
- generic [ref=f6e130]: 영향 3
- textbox "영향 3" [ref=f6e131]: 재시작과 replica 이동을 견딜 공유 저장소와 저장 token 암호화, 암호화 key 교체를 설계해야 하는데 아직 정하지 않은 문제로 남아 있다.
- generic [ref=f6e132]:
- button "위로" [ref=f6e133]
- button "아래로" [ref=f6e134]
- button "삭제" [ref=f6e135]
- generic [ref=f6e136]:
- generic [ref=f6e137]:
- generic [ref=f6e138]: 영향 4
- textbox "영향 4" [ref=f6e139]: logout이 애플리케이션 session과 authorized client를 함께 지워야 하는데, 열쇠가 달라서 한 번의 삭제로 두 상태가 함께 지워지지 않는다.
- generic [ref=f6e140]:
- button "위로" [ref=f6e141]
- button "아래로" [ref=f6e142]
- button "삭제" [ref=f6e143]
- generic [ref=f6e144]:
- generic [ref=f6e145]:
- generic [ref=f6e146]: 영향 5
- textbox "영향 5" [ref=f6e147]: 모든 UI 요청이 BFF를 지나게 되어서 지연과 단일 장애 지점을 준비해야 한다.
- generic [ref=f6e148]:
- button "위로" [ref=f6e149]
- button "아래로" [ref=f6e150]
- button "삭제" [ref=f6e151]
- generic [ref=f6e152]:
- generic [ref=f6e153]:
- generic [ref=f6e154]: 영향 6
- textbox "영향 6" [ref=f6e155]: 브라우저에서 token을 없애도 XSS가 무해해지지 않고, same-origin script는 피해자 session으로 BFF를 그대로 부를 수 있다.
- generic [ref=f6e156]:
- button "위로" [ref=f6e157]
- button "아래로" [ref=f6e158]
- button "삭제" [ref=f6e159]
- generic [ref=f6e160]:
- generic [ref=f6e161]:
- generic [ref=f6e162]: 영향 7
- textbox "영향 7" [ref=f6e163]: 이 결정이 PROPOSED인 동안은 「BFF 인증 구조 설계 기준」 Reference가 실제 적용 기준이다.
- generic [ref=f6e164]:
- button "위로" [ref=f6e165]
- button "아래로" [disabled] [ref=f6e166]
- button "삭제" [ref=f6e167]
- button "영향 추가" [ref=f6e168]
- region [ref=f6e169]:
- generic [ref=f6e170]:
- paragraph [ref=f6e171]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f6e172]
- generic [ref=f6e175]:
- navigation "문서 경로" [ref=f6e176]:
- link "Project" [ref=f6e177] [cursor=pointer]:
- /url: /projects
- generic [ref=f6e178]: /
- link "KeyCloak Patterns" [ref=f6e179] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- generic [ref=f6e180]: /
- link "Decision" [ref=f6e181] [cursor=pointer]:
- /url: /projects/keycloak-patterns/decisions
- list [ref=f6e182]:
- listitem [ref=f6e183]:
- article [ref=f6e184]:
- generic [ref=f6e185]:
- generic [ref=f6e186]:
- generic [ref=f6e187]: PROPOSED
- generic [ref=f6e188]: 결정일 미정
- heading "BFF가 OAuth Token을 관리하는 조건" [level=2] [ref=f6e189]
- paragraph [ref=f6e190]: 브라우저에 OAuth token을 노출하지 않으면서 애플리케이션이 Resource Server 호출을 중계하고 조합해야 하는 경우, BFF가 authorization code 교환과 token 보관, downstream API 호출을 소유한다.브라우저에는 애플리케이션 session만 제공한다.
- generic [ref=f6e191]:
- heading "판단 이유" [level=3] [ref=f6e192]
- paragraph [ref=f6e193]: "브라우저에 OAuth token을 전달하지 않으려면 server가 authorization code를 교환하고 access token을 사용해 downstream API를 호출해야 한다.Mediator 구조에서는 브라우저가 Resource Server를 직접 호출하므로 access token을 `/token/access` 응답으로 전달한다. 따라서 브라우저에 OAuth token을 제공하지 않는다는 요구에는 맞지 않는다.Forward-Auth 구조도 브라우저에 OAuth token을 전달하지 않을 수 있지만 upstream은 JWT를 직접 검증하지 않고 edge가 제공한 identity header를 사용한다. 애플리케이션이 access token으로 여러 Resource Server를 직접 호출하거나 사용자별 API 조합을 처리해야 한다면 BFF 쪽이 요구에 더 잘 맞는다.따라서 브라우저에 OAuth token을 전달하지 않는 조건만으로 BFF를 선택하지는 않는다. 애플리케이션이 downstream API 호출과 조합을 직접 맡아야 하는지도 함께 본다.다만 상태를 ADOPTED로 올리지는 않는다. 지금 자료는 네 구조를 나란히 실행한 비교 실험이고 이 프로젝트가 BFF를 기본값으로 고른 기록이 없기 때문이다. 기본값으로 고른 시점과 그 근거가 생기면 그때 올리게 되고, 그 전까지 실제 적용 기준은 「BFF 인증 구조 설계 기준」 Reference다."
- generic [ref=f6e194]:
- heading "영향" [level=3] [ref=f6e195]
- list [ref=f6e196]:
- listitem [ref=f6e197]: BFF가 로그인 상태와 token을 가진 보안 구성요소가 되어서 단순 proxy로 취급할 수 없게 된다.
- listitem [ref=f6e198]: 상태 변경 요청마다 CSRF 검증이 필요해지고, 노출 값과 제출 값이 다를 수 있어서 클라이언트 코드도 그 구분을 알아야 한다.
- listitem [ref=f6e199]: 재시작과 replica 이동을 견딜 공유 저장소와 저장 token 암호화, 암호화 key 교체를 설계해야 하는데 아직 정하지 않은 문제로 남아 있다.
- listitem [ref=f6e200]: logout이 애플리케이션 session과 authorized client를 함께 지워야 하는데, 열쇠가 달라서 한 번의 삭제로 두 상태가 함께 지워지지 않는다.
- listitem [ref=f6e201]: 모든 UI 요청이 BFF를 지나게 되어서 지연과 단일 장애 지점을 준비해야 한다.
- listitem [ref=f6e202]: 브라우저에서 token을 없애도 XSS가 무해해지지 않고, same-origin script는 피해자 session으로 BFF를 그대로 부를 수 있다.
- listitem [ref=f6e203]: 이 결정이 PROPOSED인 동안은 「BFF 인증 구조 설계 기준」 Reference가 실제 적용 기준이다.
- generic [ref=f6e204]:
- heading "근거 기록" [level=3] [ref=f6e205]
- list [ref=f6e206]:
- listitem [ref=f6e207]:
- link "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f6e208] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- listitem [ref=f6e209]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f6e210] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- complementary [ref=f6e211]:
- heading "작업 상태" [level=2] [ref=f6e212]
- status "편집 상태" [ref=f6e213]: 저장됨
- generic [ref=f6e214]:
- generic [ref=f6e215]:
- term [ref=f6e216]: 저장 버전
- definition [ref=f6e217]: "12"
- generic [ref=f6e218]:
- term [ref=f6e219]: 종류
- definition [ref=f6e220]: Decision
- paragraph [ref=f6e221]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f6e222]:
- button "저장" [disabled] [ref=f6e223]
- button "게시" [ref=f6e224]
- paragraph [ref=f6e225]: 버전 12으로 저장했습니다.
@@ -0,0 +1,428 @@
- generic [ref=f7e3]:
- link "본문으로 건너뛰기" [ref=f7e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f7e5]:
- generic [ref=f7e6]:
- link "TechLog Studio" [ref=f7e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f7e8]: Studio
- navigation "Studio 주 탐색" [ref=f7e10]:
- link "작업본" [ref=f7e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f7e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f7e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f7e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f7e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f7e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f7e17]
- main [ref=f7e18]:
- generic [ref=f7e19]:
- generic [ref=f7e20]:
- region [ref=f7e21]:
- generic [ref=f7e22]:
- paragraph [ref=f7e23]: QUESTION · VERSION 10
- heading "문서 편집" [level=1] [ref=f7e24]
- paragraph [ref=f7e25]: Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가
- region [ref=f7e26]:
- generic [ref=f7e27]:
- paragraph [ref=f7e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f7e29]
- generic [ref=f7e30]:
- generic [ref=f7e31]:
- generic [ref=f7e32]: 제목
- textbox "제목" [ref=f7e33]: Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가
- generic [ref=f7e34]:
- generic [ref=f7e35]: slug
- textbox "slug" [ref=f7e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: edge-authorization-scope
- generic [ref=f7e37]:
- generic [ref=f7e38]: 요약
- textbox "요약" [ref=f7e39]: 지금 edge는 user와 email만 전달하고 upstream은 role 판단을 하지 않는다. 다음 요구가 들어왔을 때 role까지 헤더로 보낼지, 아니면 인가를 애플리케이션으로 되돌릴지 정하지 않았다.
- generic [ref=f7e40]:
- generic [ref=f7e41]: Topic
- combobox "Topic" [ref=f7e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f7e43]:
- generic [ref=f7e44]: Project
- combobox "Project" [ref=f7e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f7e46]:
- generic [ref=f7e48]:
- generic [ref=f7e49]:
- generic [ref=f7e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f7e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [disabled]
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f7e52]:
- generic [ref=f7e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f7e54]: edge가 user와 email만 전달한다는 사실의 출처다.
- generic [ref=f7e55]:
- button "위로" [disabled] [ref=f7e56]
- button "아래로" [ref=f7e57]
- button "삭제" [ref=f7e58]
- generic [ref=f7e59]:
- generic [ref=f7e60]:
- generic [ref=f7e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f7e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [selected]
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f7e63]:
- generic [ref=f7e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f7e65]: 헤더 allowlist와 검증 조건이 이 기준에 있다.
- generic [ref=f7e66]:
- button "위로" [ref=f7e67]
- button "아래로" [ref=f7e68]
- button "삭제" [ref=f7e69]
- generic [ref=f7e70]:
- generic [ref=f7e71]:
- generic [ref=f7e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f7e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [disabled]
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f7e74]:
- generic [ref=f7e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f7e76]: 되돌리는 선택지의 기준이 이 문서다.
- generic [ref=f7e77]:
- button "위로" [ref=f7e78]
- button "아래로" [disabled] [ref=f7e79]
- button "삭제" [ref=f7e80]
- button "관계 추가" [ref=f7e81]
- region [ref=f7e82]:
- generic [ref=f7e83]:
- paragraph [ref=f7e84]: QUESTION
- heading "판단과 다음 검증" [level=2] [ref=f7e85]
- generic [ref=f7e86]:
- generic [ref=f7e87]: 질문 상태
- combobox "질문 상태" [ref=f7e88]:
- option "아직 정하지 않음"
- option "OPEN" [selected]
- option "RESOLVED"
- group "사실" [ref=f7e89]:
- generic [ref=f7e91]:
- generic [ref=f7e92]:
- generic [ref=f7e93]: 사실 1
- textbox "사실 1" [ref=f7e94]: 지금 edge 응답은 user와 email만 전달한다. role과 groups, tenant, 인증 방식, token 만료는 전달하지 않는다.
- generic [ref=f7e95]:
- button "위로" [disabled] [ref=f7e96]
- button "아래로" [ref=f7e97]
- button "삭제" [ref=f7e98]
- generic [ref=f7e99]:
- generic [ref=f7e100]:
- generic [ref=f7e101]: 사실 2
- textbox "사실 2" [ref=f7e102]: upstream의 identity endpoint는 role 판단을 하지 않고 누가 왔는지만 응답에 담는다.
- generic [ref=f7e103]:
- button "위로" [ref=f7e104]
- button "아래로" [ref=f7e105]
- button "삭제" [ref=f7e106]
- generic [ref=f7e107]:
- generic [ref=f7e108]:
- generic [ref=f7e109]: 사실 3
- textbox "사실 3" [ref=f7e110]: internal token 검사가 controller 한 곳에 있고 security 설정은 그 경로 전체를 permitAll로 둔다. 새 endpoint에는 보호가 따라오지 않는다.
- generic [ref=f7e111]:
- button "위로" [ref=f7e112]
- button "아래로" [ref=f7e113]
- button "삭제" [ref=f7e114]
- generic [ref=f7e115]:
- generic [ref=f7e116]:
- generic [ref=f7e117]: 사실 4
- textbox "사실 4" [ref=f7e118]: Nginx는 client가 보낸 동명 헤더를 merge하지 않고 덮어쓴다. 늘리는 헤더도 같은 처리를 받아야 한다.
- generic [ref=f7e119]:
- button "위로" [ref=f7e120]
- button "아래로" [ref=f7e121]
- button "삭제" [ref=f7e122]
- generic [ref=f7e123]:
- generic [ref=f7e124]:
- generic [ref=f7e125]: 사실 5
- textbox "사실 5" [ref=f7e126]: upstream은 JWT를 입력으로 받지 않아서 헤더로 온 값을 스스로 검증할 수단이 없다.
- generic [ref=f7e127]:
- button "위로" [ref=f7e128]
- button "아래로" [disabled] [ref=f7e129]
- button "삭제" [ref=f7e130]
- button "사실 추가" [ref=f7e131]
- group "가정" [ref=f7e132]:
- generic [ref=f7e134]:
- generic [ref=f7e135]:
- generic [ref=f7e136]: 가정 1
- textbox "가정 1" [ref=f7e137]: 헤더 종류가 늘어나면 정해야 할 계약도 함께 늘어난다.
- generic [ref=f7e138]:
- button "위로" [disabled] [ref=f7e139]
- button "아래로" [ref=f7e140]
- button "삭제" [ref=f7e141]
- generic [ref=f7e142]:
- generic [ref=f7e143]:
- generic [ref=f7e144]: 가정 2
- textbox "가정 2" [ref=f7e145]: role이 바뀌는 시점과 요청이 오는 시점이 달라서 그 사이에 들어온 요청은 옛 값을 본다.
- generic [ref=f7e146]:
- button "위로" [ref=f7e147]
- button "아래로" [disabled] [ref=f7e148]
- button "삭제" [ref=f7e149]
- button "가정 추가" [ref=f7e150]
- group "미지수" [ref=f7e151]:
- generic [ref=f7e153]:
- generic [ref=f7e154]:
- generic [ref=f7e155]: 미지수 1
- textbox "미지수 1" [ref=f7e156]: 다중 값 role을 어떤 구분자와 escaping으로 보낼지. 값 안에 그 구분자가 들어오면 어떻게 되는지.
- generic [ref=f7e157]:
- button "위로" [disabled] [ref=f7e158]
- button "아래로" [ref=f7e159]
- button "삭제" [ref=f7e160]
- generic [ref=f7e161]:
- generic [ref=f7e162]:
- generic [ref=f7e163]: 미지수 2
- textbox "미지수 2" [ref=f7e164]: 헤더 크기 상한을 넘으면 무엇이 먼저 깨지는지. proxy가 자르는지 요청 자체가 거부되는지.
- generic [ref=f7e165]:
- button "위로" [ref=f7e166]
- button "아래로" [ref=f7e167]
- button "삭제" [ref=f7e168]
- generic [ref=f7e169]:
- generic [ref=f7e170]:
- generic [ref=f7e171]: 미지수 3
- textbox "미지수 3" [ref=f7e172]: role이 바뀌었을 때 proxy session과 downstream 인가가 언제 따라가는지. 권한 회수가 몇 분 뒤에 반영되는지.
- generic [ref=f7e173]:
- button "위로" [ref=f7e174]
- button "아래로" [ref=f7e175]
- button "삭제" [ref=f7e176]
- generic [ref=f7e177]:
- generic [ref=f7e178]:
- generic [ref=f7e179]: 미지수 4
- textbox "미지수 4" [ref=f7e180]: upstream이 헤더 존재만 볼지 값과 service identity까지 볼지.
- generic [ref=f7e181]:
- button "위로" [ref=f7e182]
- button "아래로" [disabled] [ref=f7e183]
- button "삭제" [ref=f7e184]
- button "미지수 추가" [ref=f7e185]
- group "제약" [ref=f7e186]:
- generic [ref=f7e188]:
- generic [ref=f7e189]:
- generic [ref=f7e190]: 제약 1
- textbox "제약 1" [ref=f7e191]: 전달할 헤더는 allowlist로 고정해야 하고 client가 보낸 동명 헤더는 언제나 덮어써야 한다.
- generic [ref=f7e192]:
- button "위로" [disabled] [ref=f7e193]
- button "아래로" [ref=f7e194]
- button "삭제" [ref=f7e195]
- generic [ref=f7e196]:
- generic [ref=f7e197]:
- generic [ref=f7e198]: 제약 2
- textbox "제약 2" [ref=f7e199]: internal token 검사가 controller 한 곳에만 있다. 헤더를 늘리기 전에 이 검사를 공통 경계로 옮기는 것이 먼저다.
- generic [ref=f7e200]:
- button "위로" [ref=f7e201]
- button "아래로" [ref=f7e202]
- button "삭제" [ref=f7e203]
- generic [ref=f7e204]:
- generic [ref=f7e205]:
- generic [ref=f7e206]: 제약 3
- textbox "제약 3" [ref=f7e207]: upstream을 고칠 수 없어서 이 구조를 골랐다면 BFF로 되돌리는 선택지는 없다.
- generic [ref=f7e208]:
- button "위로" [ref=f7e209]
- button "아래로" [disabled] [ref=f7e210]
- button "삭제" [ref=f7e211]
- button "제약 추가" [ref=f7e212]
- group "선택지" [ref=f7e213]:
- generic [ref=f7e215]:
- generic [ref=f7e216]:
- generic [ref=f7e217]: 선택지 1 제목
- textbox "선택지 1 제목" [ref=f7e218]: 현재 — 인증만 edge에 둔다
- generic [ref=f7e219]:
- generic [ref=f7e220]: 선택지 1 설명
- textbox "선택지 1 설명" [ref=f7e221]: 헤더가 user와 email 둘로 고정돼 있어서 계약이 가장 작고 크기 상한 문제도 생기지 않는다. 인가는 upstream이 자기 저장소로 해결한다. 서비스마다 권한 조회를 따로 붙여야 한다.
- generic [ref=f7e222]:
- button "위로" [disabled] [ref=f7e223]
- button "아래로" [ref=f7e224]
- button "삭제" [ref=f7e225]
- generic [ref=f7e226]:
- generic [ref=f7e227]:
- generic [ref=f7e228]: 선택지 2 제목
- textbox "선택지 2 제목" [ref=f7e229]: 다음 후보 — role 전달까지 edge에 둔다
- generic [ref=f7e230]:
- generic [ref=f7e231]: 선택지 2 설명
- textbox "선택지 2 설명" [ref=f7e232]: 공통 role을 한 곳에서 주면 서비스마다 권한을 조회하지 않아도 된다. 이 선택을 하면 다중 값 직렬화와 크기 상한, 갱신 시점 계약을 먼저 정해야 한다. upstream은 그 값을 검증할 수단이 없어서 edge가 틀리면 그대로 틀린다.
- generic [ref=f7e233]:
- button "위로" [ref=f7e234]
- button "아래로" [ref=f7e235]
- button "삭제" [ref=f7e236]
- generic [ref=f7e237]:
- generic [ref=f7e238]:
- generic [ref=f7e239]: 선택지 3 제목
- textbox "선택지 3 제목" [ref=f7e240]: 보류 — tenant와 인가 판단까지 edge에 둔다
- generic [ref=f7e241]:
- generic [ref=f7e242]: 선택지 3 설명
- textbox "선택지 3 설명" [ref=f7e243]: tenant는 잘못 들어간 값 하나가 다른 조직의 데이터를 그대로 열어 준다. 이 값만은 upstream이 다시 확인할 수단을 함께 설계해야 해서 지금 구성으로는 감당할 수 없다. 인가 판단까지 옮기면 edge가 애플리케이션 도메인을 알아야 하고 정책이 바뀔 때마다 edge를 배포하게 된다.
- generic [ref=f7e244]:
- button "위로" [ref=f7e245]
- button "아래로" [ref=f7e246]
- button "삭제" [ref=f7e247]
- generic [ref=f7e248]:
- generic [ref=f7e249]:
- generic [ref=f7e250]: 선택지 4 제목
- textbox "선택지 4 제목" [ref=f7e251]: 경계가 커지면 — BFF로 되돌린다
- generic [ref=f7e252]:
- generic [ref=f7e253]: 선택지 4 설명
- textbox "선택지 4 설명" [ref=f7e254]: role·tenant 정보를 edge header로 계속 확장하지 않고 BFF가 필요한 정보를 조회해 인가와 API 조합을 처리하는 선택지도 있다. 이 경우 BFF session, CSRF 검증, shared store 운영이 다시 필요하다.
- generic [ref=f7e255]:
- button "위로" [ref=f7e256]
- button "아래로" [disabled] [ref=f7e257]
- button "삭제" [ref=f7e258]
- button "선택지 추가" [ref=f7e259]
- generic [ref=f7e260]:
- generic [ref=f7e261]: 다음 검증
- textbox "다음 검증" [ref=f7e262]: upstream이 실제로 요구하는 claim을 먼저 적는다. 그 목록을 놓고 아래를 본다. 1. 전달하려는 claim이 계속 늘어나는가. 2. role이나 tenant 변경이 즉시 반영돼야 하는가. 3. 정책이 애플리케이션 도메인을 알아야 하는가. 4. 헤더 값이 인가 판단의 근거가 되는가. 5. 서비스별 정책 차이가 커지는가. 2번부터 5번 중 하나라도 그렇다면 헤더를 늘리는 방향이 아니라 되돌리는 방향을 본다. role을 헤더로 실은 구성을 먼저 만들어 다중 값과 크기 상한을 넣고 무엇이 먼저 깨지는지 확인한다. role을 바꾼 뒤 몇 번째 요청부터 반영되는지도 잰다.
- region [ref=f7e263]:
- generic [ref=f7e264]:
- paragraph [ref=f7e265]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f7e266]
- generic [ref=f7e269]:
- generic [ref=f7e270]:
- navigation "문서 경로" [ref=f7e271]:
- link "Open Question" [ref=f7e272] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f7e273]: /
- generic [ref=f7e274]: OAuth/OIDC 인증 경계
- generic [ref=f7e275]: /
- link "KeyCloak Patterns" [ref=f7e276] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" [level=1] [ref=f7e277]
- paragraph [ref=f7e278]: 지금 edge는 user와 email만 전달하고 upstream은 role 판단을 하지 않는다. 다음 요구가 들어왔을 때 role까지 헤더로 보낼지, 아니면 인가를 애플리케이션으로 되돌릴지 정하지 않았다.
- generic [ref=f7e279]:
- generic [ref=f7e280]:
- term [ref=f7e281]: 유형
- definition [ref=f7e282]: Open Question
- generic [ref=f7e283]:
- term [ref=f7e284]: 프로젝트
- definition [ref=f7e285]: KeyCloak Patterns
- generic [ref=f7e286]:
- term [ref=f7e287]: 게시
- definition [ref=f7e288]: 게시 전
- paragraph [ref=f7e289]: OPEN
- article [ref=f7e290]:
- region [ref=f7e291]:
- heading "확인한 사실" [level=2] [ref=f7e292]
- list [ref=f7e293]:
- listitem [ref=f7e294]: 지금 edge 응답은 user와 email만 전달한다. role과 groups, tenant, 인증 방식, token 만료는 전달하지 않는다.
- listitem [ref=f7e295]: upstream의 identity endpoint는 role 판단을 하지 않고 누가 왔는지만 응답에 담는다.
- listitem [ref=f7e296]: internal token 검사가 controller 한 곳에 있고 security 설정은 그 경로 전체를 permitAll로 둔다. 새 endpoint에는 보호가 따라오지 않는다.
- listitem [ref=f7e297]: Nginx는 client가 보낸 동명 헤더를 merge하지 않고 덮어쓴다. 늘리는 헤더도 같은 처리를 받아야 한다.
- listitem [ref=f7e298]: upstream은 JWT를 입력으로 받지 않아서 헤더로 온 값을 스스로 검증할 수단이 없다.
- region [ref=f7e299]:
- heading "가정" [level=2] [ref=f7e300]
- list [ref=f7e301]:
- listitem [ref=f7e302]: 헤더 종류가 늘어나면 정해야 할 계약도 함께 늘어난다.
- listitem [ref=f7e303]: role이 바뀌는 시점과 요청이 오는 시점이 달라서 그 사이에 들어온 요청은 옛 값을 본다.
- region [ref=f7e304]:
- heading "남은 미지수" [level=2] [ref=f7e305]
- list [ref=f7e306]:
- listitem [ref=f7e307]: 다중 값 role을 어떤 구분자와 escaping으로 보낼지. 값 안에 그 구분자가 들어오면 어떻게 되는지.
- listitem [ref=f7e308]: 헤더 크기 상한을 넘으면 무엇이 먼저 깨지는지. proxy가 자르는지 요청 자체가 거부되는지.
- listitem [ref=f7e309]: role이 바뀌었을 때 proxy session과 downstream 인가가 언제 따라가는지. 권한 회수가 몇 분 뒤에 반영되는지.
- listitem [ref=f7e310]: upstream이 헤더 존재만 볼지 값과 service identity까지 볼지.
- region [ref=f7e311]:
- heading "제약" [level=2] [ref=f7e312]
- list [ref=f7e313]:
- listitem [ref=f7e314]: 전달할 헤더는 allowlist로 고정해야 하고 client가 보낸 동명 헤더는 언제나 덮어써야 한다.
- listitem [ref=f7e315]: internal token 검사가 controller 한 곳에만 있다. 헤더를 늘리기 전에 이 검사를 공통 경계로 옮기는 것이 먼저다.
- listitem [ref=f7e316]: upstream을 고칠 수 없어서 이 구조를 골랐다면 BFF로 되돌리는 선택지는 없다.
- region [ref=f7e317]:
- heading "검토한 선택지" [level=2] [ref=f7e318]
- list [ref=f7e319]:
- listitem [ref=f7e320]:
- heading "현재 — 인증만 edge에 둔다" [level=3] [ref=f7e321]
- paragraph [ref=f7e322]: 헤더가 user와 email 둘로 고정돼 있어서 계약이 가장 작고 크기 상한 문제도 생기지 않는다. 인가는 upstream이 자기 저장소로 해결한다. 서비스마다 권한 조회를 따로 붙여야 한다.
- listitem [ref=f7e323]:
- heading "다음 후보 — role 전달까지 edge에 둔다" [level=3] [ref=f7e324]
- paragraph [ref=f7e325]: 공통 role을 한 곳에서 주면 서비스마다 권한을 조회하지 않아도 된다. 이 선택을 하면 다중 값 직렬화와 크기 상한, 갱신 시점 계약을 먼저 정해야 한다. upstream은 그 값을 검증할 수단이 없어서 edge가 틀리면 그대로 틀린다.
- listitem [ref=f7e326]:
- heading "보류 — tenant와 인가 판단까지 edge에 둔다" [level=3] [ref=f7e327]
- paragraph [ref=f7e328]: tenant는 잘못 들어간 값 하나가 다른 조직의 데이터를 그대로 열어 준다. 이 값만은 upstream이 다시 확인할 수단을 함께 설계해야 해서 지금 구성으로는 감당할 수 없다. 인가 판단까지 옮기면 edge가 애플리케이션 도메인을 알아야 하고 정책이 바뀔 때마다 edge를 배포하게 된다.
- listitem [ref=f7e329]:
- heading "경계가 커지면 — BFF로 되돌린다" [level=3] [ref=f7e330]
- paragraph [ref=f7e331]: role·tenant 정보를 edge header로 계속 확장하지 않고 BFF가 필요한 정보를 조회해 인가와 API 조합을 처리하는 선택지도 있다. 이 경우 BFF session, CSRF 검증, shared store 운영이 다시 필요하다.
- region [ref=f7e332]:
- paragraph [ref=f7e333]: Next
- heading "다음 검증" [level=2] [ref=f7e334]
- paragraph [ref=f7e335]: upstream이 실제로 요구하는 claim을 먼저 적는다. 그 목록을 놓고 아래를 본다.1. 전달하려는 claim이 계속 늘어나는가.2. role이나 tenant 변경이 즉시 반영돼야 하는가.3. 정책이 애플리케이션 도메인을 알아야 하는가.4. 헤더 값이 인가 판단의 근거가 되는가.5. 서비스별 정책 차이가 커지는가.2번부터 5번 중 하나라도 그렇다면 헤더를 늘리는 방향이 아니라 되돌리는 방향을 본다.role을 헤더로 실은 구성을 먼저 만들어 다중 값과 크기 상한을 넣고 무엇이 먼저 깨지는지 확인한다. role을 바꾼 뒤 몇 번째 요청부터 반영되는지도 잰다.
- region [ref=f7e336]:
- paragraph [ref=f7e337]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f7e338]
- list [ref=f7e339]:
- listitem [ref=f7e340]:
- link "edge가 user와 email만 전달한다는 사실의 출처다. Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f7e341] [cursor=pointer]:
- /url: /cases/identity-header-trust
- generic [ref=f7e342]: edge가 user와 email만 전달한다는 사실의 출처다.
- strong [ref=f7e343]: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
- generic [ref=f7e344]:
- complementary [ref=f7e345]:
- heading "작업 상태" [level=2] [ref=f7e346]
- status "편집 상태" [ref=f7e347]: 저장됨
- generic [ref=f7e348]:
- generic [ref=f7e349]:
- term [ref=f7e350]: 저장 버전
- definition [ref=f7e351]: "10"
- generic [ref=f7e352]:
- term [ref=f7e353]: 종류
- definition [ref=f7e354]: QUESTION
- paragraph [ref=f7e355]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f7e356]:
- button "저장" [disabled] [ref=f7e357]
- button "게시" [ref=f7e358]
- paragraph [ref=f7e359]: 버전 10으로 저장했습니다.
@@ -0,0 +1,485 @@
- generic [ref=f8e3]:
- link "본문으로 건너뛰기" [ref=f8e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f8e5]:
- generic [ref=f8e6]:
- link "TechLog Studio" [ref=f8e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f8e8]: Studio
- navigation "Studio 주 탐색" [ref=f8e10]:
- link "작업본" [ref=f8e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f8e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f8e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f8e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f8e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f8e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f8e17]
- main [ref=f8e18]:
- generic [ref=f8e19]:
- generic [ref=f8e20]:
- region [ref=f8e21]:
- generic [ref=f8e22]:
- paragraph [ref=f8e23]: QUESTION · VERSION 9
- heading "문서 편집" [level=1] [ref=f8e24]
- paragraph [ref=f8e25]: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
- region [ref=f8e26]:
- generic [ref=f8e27]:
- paragraph [ref=f8e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f8e29]
- generic [ref=f8e30]:
- generic [ref=f8e31]:
- generic [ref=f8e32]: 제목
- textbox "제목" [ref=f8e33]: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
- generic [ref=f8e34]:
- generic [ref=f8e35]: slug
- textbox "slug" [ref=f8e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: bff-session-authorized-client-store
- generic [ref=f8e37]:
- generic [ref=f8e38]: 요약
- textbox "요약" [ref=f8e39]: session과 authorized client는 찾는 열쇠가 달라서 같은 저장소에 두는 것이 당연하지 않다. 저장소 후보는 Redis 쪽으로 기울어 있지만 token 암호화와 만료 정합, logout 정리를 확인하지 않았다.
- generic [ref=f8e40]:
- generic [ref=f8e41]: Topic
- combobox "Topic" [ref=f8e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f8e43]:
- generic [ref=f8e44]: Project
- combobox "Project" [ref=f8e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f8e46]:
- generic [ref=f8e48]:
- generic [ref=f8e49]:
- generic [ref=f8e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f8e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [selected]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e52]:
- generic [ref=f8e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f8e54]: 이 질문에서 저장소 부분만 떼어 낸 것이다.
- generic [ref=f8e55]:
- button "위로" [disabled] [ref=f8e56]
- button "아래로" [ref=f8e57]
- button "삭제" [ref=f8e58]
- generic [ref=f8e59]:
- generic [ref=f8e60]:
- generic [ref=f8e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f8e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e63]:
- generic [ref=f8e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f8e65]: session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- generic [ref=f8e66]:
- button "위로" [ref=f8e67]
- button "아래로" [ref=f8e68]
- button "삭제" [ref=f8e69]
- generic [ref=f8e70]:
- generic [ref=f8e71]:
- generic [ref=f8e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f8e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e74]:
- generic [ref=f8e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f8e76]: 이 기준의 저장소 항목이 이 질문의 답을 기다린다.
- generic [ref=f8e77]:
- button "위로" [ref=f8e78]
- button "아래로" [ref=f8e79]
- button "삭제" [ref=f8e80]
- generic [ref=f8e81]:
- generic [ref=f8e82]:
- generic [ref=f8e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f8e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [selected]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e85]:
- generic [ref=f8e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f8e87]: 저장소를 공유한 뒤에야 replica 경쟁이 재현된다.
- generic [ref=f8e88]:
- button "위로" [ref=f8e89]
- button "아래로" [disabled] [ref=f8e90]
- button "삭제" [ref=f8e91]
- button "관계 추가" [ref=f8e92]
- region [ref=f8e93]:
- generic [ref=f8e94]:
- paragraph [ref=f8e95]: QUESTION
- heading "판단과 다음 검증" [level=2] [ref=f8e96]
- generic [ref=f8e97]:
- generic [ref=f8e98]: 질문 상태
- combobox "질문 상태" [ref=f8e99]:
- option "아직 정하지 않음"
- option "OPEN" [selected]
- option "RESOLVED"
- group "사실" [ref=f8e100]:
- generic [ref=f8e102]:
- generic [ref=f8e103]:
- generic [ref=f8e104]: 사실 1
- textbox "사실 1" [ref=f8e105]: 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- generic [ref=f8e106]:
- button "위로" [disabled] [ref=f8e107]
- button "아래로" [ref=f8e108]
- button "삭제" [ref=f8e109]
- generic [ref=f8e110]:
- generic [ref=f8e111]:
- generic [ref=f8e112]: 사실 2
- textbox "사실 2" [ref=f8e113]: 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- generic [ref=f8e114]:
- button "위로" [ref=f8e115]
- button "아래로" [ref=f8e116]
- button "삭제" [ref=f8e117]
- generic [ref=f8e118]:
- generic [ref=f8e119]:
- generic [ref=f8e120]: 사실 3
- textbox "사실 3" [ref=f8e121]: OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이고 코드가 직접 선언하지 않는다.
- generic [ref=f8e122]:
- button "위로" [ref=f8e123]
- button "아래로" [ref=f8e124]
- button "삭제" [ref=f8e125]
- generic [ref=f8e126]:
- generic [ref=f8e127]:
- generic [ref=f8e128]: 사실 4
- textbox "사실 4" [ref=f8e129]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- generic [ref=f8e130]:
- button "위로" [ref=f8e131]
- button "아래로" [ref=f8e132]
- button "삭제" [ref=f8e133]
- generic [ref=f8e134]:
- generic [ref=f8e135]:
- generic [ref=f8e136]: 사실 5
- textbox "사실 5" [ref=f8e137]: authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
- generic [ref=f8e138]:
- button "위로" [ref=f8e139]
- button "아래로" [disabled] [ref=f8e140]
- button "삭제" [ref=f8e141]
- button "사실 추가" [ref=f8e142]
- group "가정" [ref=f8e143]:
- generic [ref=f8e145]:
- generic [ref=f8e146]:
- generic [ref=f8e147]: 가정 1
- textbox "가정 1" [ref=f8e148]: 두 상태를 같은 저장소에 둘 필요는 없다.
- generic [ref=f8e149]:
- button "위로" [disabled] [ref=f8e150]
- button "아래로" [ref=f8e151]
- button "삭제" [ref=f8e152]
- generic [ref=f8e153]:
- generic [ref=f8e154]:
- generic [ref=f8e155]: 가정 2
- textbox "가정 2" [ref=f8e156]: 저장된 refresh token을 평문으로 두면 안 된다.
- generic [ref=f8e157]:
- button "위로" [ref=f8e158]
- button "아래로" [ref=f8e159]
- button "삭제" [ref=f8e160]
- generic [ref=f8e161]:
- generic [ref=f8e162]:
- generic [ref=f8e163]: 가정 3
- textbox "가정 3" [ref=f8e164]: session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
- generic [ref=f8e165]:
- button "위로" [ref=f8e166]
- button "아래로" [disabled] [ref=f8e167]
- button "삭제" [ref=f8e168]
- button "가정 추가" [ref=f8e169]
- group "미지수" [ref=f8e170]:
- generic [ref=f8e172]:
- generic [ref=f8e173]:
- generic [ref=f8e174]: 미지수 1
- textbox "미지수 1" [ref=f8e175]: Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- generic [ref=f8e176]:
- button "위로" [disabled] [ref=f8e177]
- button "아래로" [ref=f8e178]
- button "삭제" [ref=f8e179]
- generic [ref=f8e180]:
- generic [ref=f8e181]:
- generic [ref=f8e182]: 미지수 2
- textbox "미지수 2" [ref=f8e183]: session과 authorized client를 같은 store에 둘지 나눌지.
- generic [ref=f8e184]:
- button "위로" [ref=f8e185]
- button "아래로" [ref=f8e186]
- button "삭제" [ref=f8e187]
- generic [ref=f8e188]:
- generic [ref=f8e189]:
- generic [ref=f8e190]: 미지수 3
- textbox "미지수 3" [ref=f8e191]: 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- generic [ref=f8e192]:
- button "위로" [ref=f8e193]
- button "아래로" [ref=f8e194]
- button "삭제" [ref=f8e195]
- generic [ref=f8e196]:
- generic [ref=f8e197]:
- generic [ref=f8e198]: 미지수 4
- textbox "미지수 4" [ref=f8e199]: session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- generic [ref=f8e200]:
- button "위로" [ref=f8e201]
- button "아래로" [ref=f8e202]
- button "삭제" [ref=f8e203]
- generic [ref=f8e204]:
- generic [ref=f8e205]:
- generic [ref=f8e206]: 미지수 5
- textbox "미지수 5" [ref=f8e207]: 열쇠가 다른 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- generic [ref=f8e208]:
- button "위로" [ref=f8e209]
- button "아래로" [ref=f8e210]
- button "삭제" [ref=f8e211]
- generic [ref=f8e212]:
- generic [ref=f8e213]:
- generic [ref=f8e214]: 미지수 6
- textbox "미지수 6" [ref=f8e215]: sticky session이 durable store의 대안이 되는가 보완이 되는가.
- generic [ref=f8e216]:
- button "위로" [ref=f8e217]
- button "아래로" [disabled] [ref=f8e218]
- button "삭제" [ref=f8e219]
- button "미지수 추가" [ref=f8e220]
- group "제약" [ref=f8e221]:
- generic [ref=f8e223]:
- generic [ref=f8e224]:
- generic [ref=f8e225]: 제약 1
- textbox "제약 1" [ref=f8e226]: authorized client의 열쇠에 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token 항목을 보게 된다.
- generic [ref=f8e227]:
- button "위로" [disabled] [ref=f8e228]
- button "아래로" [ref=f8e229]
- button "삭제" [ref=f8e230]
- generic [ref=f8e231]:
- generic [ref=f8e232]:
- generic [ref=f8e233]: 제약 2
- textbox "제약 2" [ref=f8e234]: 커밋된 테스트에 저장소 관련 계약이 없어서 어느 후보를 골라도 지금은 회귀를 잡아 줄 검사가 없다.
- generic [ref=f8e235]:
- button "위로" [ref=f8e236]
- button "아래로" [ref=f8e237]
- button "삭제" [ref=f8e238]
- generic [ref=f8e239]:
- generic [ref=f8e240]:
- generic [ref=f8e241]: 제약 3
- textbox "제약 3" [ref=f8e242]: 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
- generic [ref=f8e243]:
- button "위로" [ref=f8e244]
- button "아래로" [disabled] [ref=f8e245]
- button "삭제" [ref=f8e246]
- button "제약 추가" [ref=f8e247]
- group "선택지" [ref=f8e248]:
- generic [ref=f8e250]:
- generic [ref=f8e251]:
- generic [ref=f8e252]: 선택지 1 제목
- textbox "선택지 1 제목" [ref=f8e253]: 유력 후보 — session과 authorized client를 모두 Redis에 둔다
- generic [ref=f8e254]:
- generic [ref=f8e255]: 선택지 1 설명
- textbox "선택지 1 설명" [ref=f8e256]: Spring Session Redis와 Redis authorized-client repository를 쓰게 되면 만료를 store가 관리해 주고 인스턴스를 늘리기도 쉬워진다. Redis를 사용하면 모든 replica가 같은 session과 authorized client를 조회할 수 있다. 인증 경로가 Redis 가용성에 의존하게 되며, access·refresh token 저장 시 암호화 여부와 key 관리 방식도 정해야 한다.
- generic [ref=f8e257]:
- button "위로" [disabled] [ref=f8e258]
- button "아래로" [ref=f8e259]
- button "삭제" [ref=f8e260]
- generic [ref=f8e261]:
- generic [ref=f8e262]:
- generic [ref=f8e263]: 선택지 2 제목
- textbox "선택지 2 제목" [ref=f8e264]: session과 authorized client를 모두 JDBC에 둔다
- generic [ref=f8e265]:
- generic [ref=f8e266]: 선택지 2 설명
- textbox "선택지 2 설명" [ref=f8e267]: 이미 운영 중인 DB를 쓴다. 백업과 감사 절차가 그 DB에 이미 있다면 그만큼 새로 만들 것이 줄어든다. JDBC를 사용하면 기존 관계형 DB 운영 체계를 활용할 수 있지만 인증 요청마다 DB 조회가 발생한다. 만료 데이터 정리와 session 조회 지연도 운영 항목으로 포함해야 한다.
- generic [ref=f8e268]:
- button "위로" [ref=f8e269]
- button "아래로" [ref=f8e270]
- button "삭제" [ref=f8e271]
- generic [ref=f8e272]:
- generic [ref=f8e273]:
- generic [ref=f8e274]: 선택지 3 제목
- textbox "선택지 3 제목" [ref=f8e275]: 변경이 가장 작은 안 — session만 공유하고 sticky session을 쓴다
- generic [ref=f8e276]:
- generic [ref=f8e277]: 선택지 3 설명
- textbox "선택지 3 설명" [ref=f8e278]: Spring Session만 붙이면 되어서 변경이 가장 적다. sticky session만 적용하면 authorized client는 여전히 process-local 상태다. 요청이 다른 인스턴스로 라우팅되거나 해당 인스턴스가 종료될 때 session과 token 상태의 정합을 보장하기 어렵다.
- generic [ref=f8e279]:
- button "위로" [ref=f8e280]
- button "아래로" [ref=f8e281]
- button "삭제" [ref=f8e282]
- generic [ref=f8e283]:
- generic [ref=f8e284]:
- generic [ref=f8e285]: 선택지 4 제목
- textbox "선택지 4 제목" [ref=f8e286]: session은 Redis, token은 암호화한 JDBC에 둔다
- generic [ref=f8e287]:
- generic [ref=f8e288]: 선택지 4 설명
- textbox "선택지 4 설명" [ref=f8e289]: 요청마다 읽는 session은 빠른 저장소에 두고 오래 보관하면서 암호화가 필요한 token은 DB에 두게 되어서 접근 패턴에 맞다. session과 authorized client를 서로 다른 저장소에 두면 각각의 TTL과 logout 정리 순서를 맞춰야 하고 운영 대상 저장소도 하나 늘어난다.
- generic [ref=f8e290]:
- button "위로" [ref=f8e291]
- button "아래로" [disabled] [ref=f8e292]
- button "삭제" [ref=f8e293]
- button "선택지 추가" [ref=f8e294]
- generic [ref=f8e295]:
- generic [ref=f8e296]: 다음 검증
- textbox "다음 검증" [ref=f8e297]: 후보마다 같은 입력으로 재서 비교한다. 1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다. 2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다. 3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다. 4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다. 5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다. 암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
- region [ref=f8e298]:
- generic [ref=f8e299]:
- paragraph [ref=f8e300]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f8e301]
- generic [ref=f8e304]:
- generic [ref=f8e305]:
- navigation "문서 경로" [ref=f8e306]:
- link "Open Question" [ref=f8e307] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f8e308]: /
- generic [ref=f8e309]: OAuth/OIDC 인증 경계
- generic [ref=f8e310]: /
- link "KeyCloak Patterns" [ref=f8e311] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [level=1] [ref=f8e312]
- paragraph [ref=f8e313]: session과 authorized client는 찾는 열쇠가 달라서 같은 저장소에 두는 것이 당연하지 않다. 저장소 후보는 Redis 쪽으로 기울어 있지만 token 암호화와 만료 정합, logout 정리를 확인하지 않았다.
- generic [ref=f8e314]:
- generic [ref=f8e315]:
- term [ref=f8e316]: 유형
- definition [ref=f8e317]: Open Question
- generic [ref=f8e318]:
- term [ref=f8e319]: 프로젝트
- definition [ref=f8e320]: KeyCloak Patterns
- generic [ref=f8e321]:
- term [ref=f8e322]: 게시
- definition [ref=f8e323]: 게시 전
- paragraph [ref=f8e324]: OPEN
- article [ref=f8e325]:
- region [ref=f8e326]:
- heading "확인한 사실" [level=2] [ref=f8e327]
- list [ref=f8e328]:
- listitem [ref=f8e329]: 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- listitem [ref=f8e330]: 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- listitem [ref=f8e331]: OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이고 코드가 직접 선언하지 않는다.
- listitem [ref=f8e332]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- listitem [ref=f8e333]: authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
- region [ref=f8e334]:
- heading "가정" [level=2] [ref=f8e335]
- list [ref=f8e336]:
- listitem [ref=f8e337]: 두 상태를 같은 저장소에 둘 필요는 없다.
- listitem [ref=f8e338]: 저장된 refresh token을 평문으로 두면 안 된다.
- listitem [ref=f8e339]: session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
- region [ref=f8e340]:
- heading "남은 미지수" [level=2] [ref=f8e341]
- list [ref=f8e342]:
- listitem [ref=f8e343]: Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- listitem [ref=f8e344]: session과 authorized client를 같은 store에 둘지 나눌지.
- listitem [ref=f8e345]: 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- listitem [ref=f8e346]: session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- listitem [ref=f8e347]: 열쇠가 다른 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- listitem [ref=f8e348]: sticky session이 durable store의 대안이 되는가 보완이 되는가.
- region [ref=f8e349]:
- heading "제약" [level=2] [ref=f8e350]
- list [ref=f8e351]:
- listitem [ref=f8e352]: authorized client의 열쇠에 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token 항목을 보게 된다.
- listitem [ref=f8e353]: 커밋된 테스트에 저장소 관련 계약이 없어서 어느 후보를 골라도 지금은 회귀를 잡아 줄 검사가 없다.
- listitem [ref=f8e354]: 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
- region [ref=f8e355]:
- heading "검토한 선택지" [level=2] [ref=f8e356]
- list [ref=f8e357]:
- listitem [ref=f8e358]:
- heading "유력 후보 — session과 authorized client를 모두 Redis에 둔다" [level=3] [ref=f8e359]
- paragraph [ref=f8e360]: Spring Session Redis와 Redis authorized-client repository를 쓰게 되면 만료를 store가 관리해 주고 인스턴스를 늘리기도 쉬워진다. Redis를 사용하면 모든 replica가 같은 session과 authorized client를 조회할 수 있다. 인증 경로가 Redis 가용성에 의존하게 되며, access·refresh token 저장 시 암호화 여부와 key 관리 방식도 정해야 한다.
- listitem [ref=f8e361]:
- heading "session과 authorized client를 모두 JDBC에 둔다" [level=3] [ref=f8e362]
- paragraph [ref=f8e363]: 이미 운영 중인 DB를 쓴다. 백업과 감사 절차가 그 DB에 이미 있다면 그만큼 새로 만들 것이 줄어든다. JDBC를 사용하면 기존 관계형 DB 운영 체계를 활용할 수 있지만 인증 요청마다 DB 조회가 발생한다. 만료 데이터 정리와 session 조회 지연도 운영 항목으로 포함해야 한다.
- listitem [ref=f8e364]:
- heading "변경이 가장 작은 안 — session만 공유하고 sticky session을 쓴다" [level=3] [ref=f8e365]
- paragraph [ref=f8e366]: Spring Session만 붙이면 되어서 변경이 가장 적다. sticky session만 적용하면 authorized client는 여전히 process-local 상태다. 요청이 다른 인스턴스로 라우팅되거나 해당 인스턴스가 종료될 때 session과 token 상태의 정합을 보장하기 어렵다.
- listitem [ref=f8e367]:
- heading "session은 Redis, token은 암호화한 JDBC에 둔다" [level=3] [ref=f8e368]
- paragraph [ref=f8e369]: 요청마다 읽는 session은 빠른 저장소에 두고 오래 보관하면서 암호화가 필요한 token은 DB에 두게 되어서 접근 패턴에 맞다. session과 authorized client를 서로 다른 저장소에 두면 각각의 TTL과 logout 정리 순서를 맞춰야 하고 운영 대상 저장소도 하나 늘어난다.
- region [ref=f8e370]:
- paragraph [ref=f8e371]: Next
- heading "다음 검증" [level=2] [ref=f8e372]
- paragraph [ref=f8e373]: 후보마다 같은 입력으로 재서 비교한다.1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
- region [ref=f8e374]:
- paragraph [ref=f8e375]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f8e376]
- list [ref=f8e377]:
- listitem [ref=f8e378]:
- link "session과 authorized client의 열쇠가 다르다는 사실의 출처다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f8e379] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f8e380]: session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- strong [ref=f8e381]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f8e382]:
- complementary [ref=f8e383]:
- heading "작업 상태" [level=2] [ref=f8e384]
- status "편집 상태" [ref=f8e385]: 저장됨
- generic [ref=f8e386]:
- generic [ref=f8e387]:
- term [ref=f8e388]: 저장 버전
- definition [ref=f8e389]: "9"
- generic [ref=f8e390]:
- term [ref=f8e391]: 종류
- definition [ref=f8e392]: QUESTION
- paragraph [ref=f8e393]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f8e394]:
- button "저장" [disabled] [ref=f8e395]
- button "게시" [ref=f8e396]
- paragraph [ref=f8e397]: 버전 9으로 저장했습니다.
@@ -0,0 +1,485 @@
- generic [ref=f9e3]:
- link "본문으로 건너뛰기" [ref=f9e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f9e5]:
- generic [ref=f9e6]:
- link "TechLog Studio" [ref=f9e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f9e8]: Studio
- navigation "Studio 주 탐색" [ref=f9e10]:
- link "작업본" [ref=f9e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f9e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f9e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f9e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f9e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f9e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f9e17]
- main [ref=f9e18]:
- generic [ref=f9e19]:
- generic [ref=f9e20]:
- region [ref=f9e21]:
- generic [ref=f9e22]:
- paragraph [ref=f9e23]: QUESTION · VERSION 11
- heading "문서 편집" [level=1] [ref=f9e24]
- paragraph [ref=f9e25]: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
- region [ref=f9e26]:
- generic [ref=f9e27]:
- paragraph [ref=f9e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f9e29]
- generic [ref=f9e30]:
- generic [ref=f9e31]:
- generic [ref=f9e32]: 제목
- textbox "제목" [ref=f9e33]: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
- generic [ref=f9e34]:
- generic [ref=f9e35]: slug
- textbox "slug" [ref=f9e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: refresh-rotation-replica-contention
- generic [ref=f9e37]:
- generic [ref=f9e38]: 요약
- textbox "요약" [ref=f9e39]: realm이 refresh token rotation과 재사용 허용 0회를 쓴다. 두 replica가 같은 refresh token으로 동시에 갱신할 수 있고, 그때 두 번째 사용이 거부될 가능성이 있다. 실제 Keycloak 응답과 session 영향은 아직 재현하지 않았다.
- generic [ref=f9e40]:
- generic [ref=f9e41]: Topic
- combobox "Topic" [ref=f9e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f9e43]:
- generic [ref=f9e44]: Project
- combobox "Project" [ref=f9e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f9e46]:
- generic [ref=f9e48]:
- generic [ref=f9e49]:
- generic [ref=f9e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f9e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f9e52]:
- generic [ref=f9e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f9e54]: 저장소 결정이 이 질문보다 앞선다.
- generic [ref=f9e55]:
- button "위로" [disabled] [ref=f9e56]
- button "아래로" [ref=f9e57]
- button "삭제" [ref=f9e58]
- generic [ref=f9e59]:
- generic [ref=f9e60]:
- generic [ref=f9e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f9e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f9e63]:
- generic [ref=f9e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f9e65]: rotation과 재사용 0회를 쓰는 구성의 출처다.
- generic [ref=f9e66]:
- button "위로" [ref=f9e67]
- button "아래로" [ref=f9e68]
- button "삭제" [ref=f9e69]
- generic [ref=f9e70]:
- generic [ref=f9e71]:
- generic [ref=f9e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f9e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [selected]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f9e74]:
- generic [ref=f9e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f9e76]: 다중 인스턴스 운영이 이 경쟁의 전제다.
- generic [ref=f9e77]:
- button "위로" [ref=f9e78]
- button "아래로" [ref=f9e79]
- button "삭제" [ref=f9e80]
- generic [ref=f9e81]:
- generic [ref=f9e82]:
- generic [ref=f9e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f9e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f9e85]:
- generic [ref=f9e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f9e87]: 갱신 실패를 화면 오류로 바꾸는 규칙이 이 기준의 항목이다.
- generic [ref=f9e88]:
- button "위로" [ref=f9e89]
- button "아래로" [disabled] [ref=f9e90]
- button "삭제" [ref=f9e91]
- button "관계 추가" [ref=f9e92]
- region [ref=f9e93]:
- generic [ref=f9e94]:
- paragraph [ref=f9e95]: QUESTION
- heading "판단과 다음 검증" [level=2] [ref=f9e96]
- generic [ref=f9e97]:
- generic [ref=f9e98]: 질문 상태
- combobox "질문 상태" [ref=f9e99]:
- option "아직 정하지 않음"
- option "OPEN" [selected]
- option "RESOLVED"
- group "사실" [ref=f9e100]:
- generic [ref=f9e102]:
- generic [ref=f9e103]:
- generic [ref=f9e104]: 사실 1
- textbox "사실 1" [ref=f9e105]: realm은 refresh token rotation과 재사용 허용 0회를 쓰게 되어서, 한 번 갱신하면 이전 refresh token은 바로 무효가 된다.
- generic [ref=f9e106]:
- button "위로" [disabled] [ref=f9e107]
- button "아래로" [ref=f9e108]
- button "삭제" [ref=f9e109]
- generic [ref=f9e110]:
- generic [ref=f9e111]:
- generic [ref=f9e112]: 사실 2
- textbox "사실 2" [ref=f9e113]: 커밋된 테스트는 새 refresh token 발급과 이전 token 거부, revocation 뒤 refresh 실패를 확인하는데 모두 한 주체가 순서대로 부르는 경우다.
- generic [ref=f9e114]:
- button "위로" [ref=f9e115]
- button "아래로" [ref=f9e116]
- button "삭제" [ref=f9e117]
- generic [ref=f9e118]:
- generic [ref=f9e119]:
- generic [ref=f9e120]: 사실 3
- textbox "사실 3" [ref=f9e121]: authorized client manager에는 refresh-token provider가 구성되어 있어 access token 만료 시 refresh를 시도할 수 있다.
- generic [ref=f9e122]:
- button "위로" [ref=f9e123]
- button "아래로" [ref=f9e124]
- button "삭제" [ref=f9e125]
- generic [ref=f9e126]:
- generic [ref=f9e127]:
- generic [ref=f9e128]: 사실 4
- textbox "사실 4" [ref=f9e129]: 다만 만료를 기다려 실제 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
- generic [ref=f9e130]:
- button "위로" [ref=f9e131]
- button "아래로" [ref=f9e132]
- button "삭제" [ref=f9e133]
- generic [ref=f9e134]:
- generic [ref=f9e135]:
- generic [ref=f9e136]: 사실 5
- textbox "사실 5" [ref=f9e137]: 현재 authorized client 저장소는 process-local이라 replica가 같은 refresh token 상태를 공유하지 않는다. 따라서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
- generic [ref=f9e138]:
- button "위로" [ref=f9e139]
- button "아래로" [ref=f9e140]
- button "삭제" [ref=f9e141]
- generic [ref=f9e142]:
- generic [ref=f9e143]:
- generic [ref=f9e144]: 사실 6
- textbox "사실 6" [ref=f9e145]: 이미 발급된 access token은 만료 전까지 API에서 계속 통하기 때문에, 갱신이 실패해도 그동안은 화면이 정상으로 보이게 된다.
- generic [ref=f9e146]:
- button "위로" [ref=f9e147]
- button "아래로" [disabled] [ref=f9e148]
- button "삭제" [ref=f9e149]
- button "사실 추가" [ref=f9e150]
- group "가정" [ref=f9e151]:
- generic [ref=f9e153]:
- generic [ref=f9e154]:
- generic [ref=f9e155]: 가정 1
- textbox "가정 1" [ref=f9e156]: 운영에서는 replica가 둘 이상이고 저장소를 공유해 같은 authorized client 항목을 보게 된다.
- generic [ref=f9e157]:
- button "위로" [disabled] [ref=f9e158]
- button "아래로" [ref=f9e159]
- button "삭제" [ref=f9e160]
- generic [ref=f9e161]:
- generic [ref=f9e162]:
- generic [ref=f9e163]: 가정 2
- textbox "가정 2" [ref=f9e164]: 두 replica가 비슷한 시각에 만료를 만나면 각각 갱신을 시도하게 된다.
- generic [ref=f9e165]:
- button "위로" [ref=f9e166]
- button "아래로" [disabled] [ref=f9e167]
- button "삭제" [ref=f9e168]
- button "가정 추가" [ref=f9e169]
- group "미지수" [ref=f9e170]:
- generic [ref=f9e172]:
- generic [ref=f9e173]:
- generic [ref=f9e174]: 미지수 1
- textbox "미지수 1" [ref=f9e175]: 같은 refresh token으로 두 replica가 동시에 갱신하면 어느 쪽이 이기고 지는 쪽은 무엇을 받게 되는가.
- generic [ref=f9e176]:
- button "위로" [disabled] [ref=f9e177]
- button "아래로" [ref=f9e178]
- button "삭제" [ref=f9e179]
- generic [ref=f9e180]:
- generic [ref=f9e181]:
- generic [ref=f9e182]: 미지수 2
- textbox "미지수 2" [ref=f9e183]: 재사용 허용 0회에서 지는 쪽의 요청이 사용자 화면에 어떻게 보이게 되는가. 로그인 만료로 보이는가 일시적 오류로 보이는가.
- generic [ref=f9e184]:
- button "위로" [ref=f9e185]
- button "아래로" [ref=f9e186]
- button "삭제" [ref=f9e187]
- generic [ref=f9e188]:
- generic [ref=f9e189]:
- generic [ref=f9e190]: 미지수 3
- textbox "미지수 3" [ref=f9e191]: 지는 쪽이 저장소에서 새 token을 다시 읽어 재시도하면 성공하게 되는가, 아니면 재인증이 필요해지는가.
- generic [ref=f9e192]:
- button "위로" [ref=f9e193]
- button "아래로" [ref=f9e194]
- button "삭제" [ref=f9e195]
- generic [ref=f9e196]:
- generic [ref=f9e197]:
- generic [ref=f9e198]: 미지수 4
- textbox "미지수 4" [ref=f9e199]: 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
- generic [ref=f9e200]:
- button "위로" [ref=f9e201]
- button "아래로" [ref=f9e202]
- button "삭제" [ref=f9e203]
- generic [ref=f9e204]:
- generic [ref=f9e205]:
- generic [ref=f9e206]: 미지수 5
- textbox "미지수 5" [ref=f9e207]: lock을 쓴다면 어디에 두고 얼마나 잡게 되는가. 잡은 채로 프로세스가 내려가면 어떻게 푸는가.
- generic [ref=f9e208]:
- button "위로" [ref=f9e209]
- button "아래로" [ref=f9e210]
- button "삭제" [ref=f9e211]
- generic [ref=f9e212]:
- generic [ref=f9e213]:
- generic [ref=f9e214]: 미지수 6
- textbox "미지수 6" [ref=f9e215]: 갱신 실패를 로그인 만료와 구분해서 표시할 수 있게 되는가.
- generic [ref=f9e216]:
- button "위로" [ref=f9e217]
- button "아래로" [disabled] [ref=f9e218]
- button "삭제" [ref=f9e219]
- button "미지수 추가" [ref=f9e220]
- group "제약" [ref=f9e221]:
- generic [ref=f9e223]:
- generic [ref=f9e224]:
- generic [ref=f9e225]: 제약 1
- textbox "제약 1" [ref=f9e226]: rotation과 재사용 0회는 이미 realm 설정이라서 이 전제를 바꾸지 않고 답해야 한다.
- generic [ref=f9e227]:
- button "위로" [disabled] [ref=f9e228]
- button "아래로" [ref=f9e229]
- button "삭제" [ref=f9e230]
- generic [ref=f9e231]:
- generic [ref=f9e232]:
- generic [ref=f9e233]: 제약 2
- textbox "제약 2" [ref=f9e234]: 이미 발급된 access token은 만료 전까지 사용할 수 있으므로 refresh 실패는 즉시 보이지 않을 수 있다. 재현 테스트는 access token 만료 직후에 맞춰 실행한다.
- generic [ref=f9e235]:
- button "위로" [ref=f9e236]
- button "아래로" [ref=f9e237]
- button "삭제" [ref=f9e238]
- generic [ref=f9e239]:
- generic [ref=f9e240]:
- generic [ref=f9e241]: 제약 3
- textbox "제약 3" [ref=f9e242]: 이 경쟁은 저장소를 공유한 뒤에야 재현되기 때문에 저장소 결정이 이 질문보다 앞서게 된다.
- generic [ref=f9e243]:
- button "위로" [ref=f9e244]
- button "아래로" [disabled] [ref=f9e245]
- button "삭제" [ref=f9e246]
- button "제약 추가" [ref=f9e247]
- group "선택지" [ref=f9e248]:
- generic [ref=f9e250]:
- generic [ref=f9e251]:
- generic [ref=f9e252]: 선택지 1 제목
- textbox "선택지 1 제목" [ref=f9e253]: 분산 lock으로 갱신을 직렬화한다
- generic [ref=f9e254]:
- generic [ref=f9e255]: 선택지 1 설명
- textbox "선택지 1 설명" [ref=f9e256]: 한 replica만 갱신하고 나머지는 끝나기를 기다렸다가 결과를 읽게 되어서 재사용 거부가 아예 생기지 않는다. 분산 lock을 사용하면 refresh 구간을 직렬화할 수 있다. lock 저장소의 가용성, lock 만료, 재진입, lock 보유 process 종료 상황까지 함께 처리해야 한다.
- generic [ref=f9e257]:
- button "위로" [disabled] [ref=f9e258]
- button "아래로" [ref=f9e259]
- button "삭제" [ref=f9e260]
- generic [ref=f9e261]:
- generic [ref=f9e262]:
- generic [ref=f9e263]: 선택지 2 제목
- textbox "선택지 2 제목" [ref=f9e264]: 각자 갱신하고 실패는 재시도로 처리한다
- generic [ref=f9e265]:
- generic [ref=f9e266]: 선택지 2 설명
- textbox "선택지 2 설명" [ref=f9e267]: 구현이 가장 단순하다. 지는 쪽이 거부를 받으면 저장소에서 최신 token을 다시 읽어 재시도한다는 전제인데, 이 재시도가 성립하는지는 아직 확인하지 않았다. reuse detection 정책에 따라 같은 refresh token의 두 번째 사용이 token family 전체에 영향을 줄 수 있다. 이 경우 단순 retry로 끝나지 않고 재인증이 필요할 수 있다. 갱신에 성공한 replica가 새 token을 저장하기 전에 다른 replica가 다시 조회하는 순서도 별도로 재현해야 한다.
- generic [ref=f9e268]:
- button "위로" [ref=f9e269]
- button "아래로" [ref=f9e270]
- button "삭제" [ref=f9e271]
- generic [ref=f9e272]:
- generic [ref=f9e273]:
- generic [ref=f9e274]: 선택지 3 제목
- textbox "선택지 3 제목" [ref=f9e275]: 갱신 전용 경로를 하나 둔다
- generic [ref=f9e276]:
- generic [ref=f9e277]: 선택지 3 설명
- textbox "선택지 3 설명" [ref=f9e278]: refresh를 전담하는 구성요소 하나만 refresh token을 사용하고 다른 replica는 갱신 결과를 조회하도록 구성할 수 있다. refresh 전담 구성요소가 중단되면 access token 만료 이후 갱신을 수행할 주체가 없어지므로 해당 구성요소의 가용성과 복구 방식이 중요해진다.
- generic [ref=f9e279]:
- button "위로" [ref=f9e280]
- button "아래로" [ref=f9e281]
- button "삭제" [ref=f9e282]
- generic [ref=f9e283]:
- generic [ref=f9e284]:
- generic [ref=f9e285]: 선택지 4 제목
- textbox "선택지 4 제목" [ref=f9e286]: 제약상 제외 — 재사용 허용을 늘린다
- generic [ref=f9e287]:
- generic [ref=f9e288]: 선택지 4 설명
- textbox "선택지 4 설명" [ref=f9e289]: 짧은 유예를 주면 경쟁이 저절로 해소되고 코드도 고칠 필요가 없다. 다만 rotation과 재사용 0회는 이 질문이 바꾸지 않기로 한 realm 설정이다. 훔친 refresh token을 쓸 수 있는 창도 같이 늘어난다. 비교 대상으로만 남긴다.
- generic [ref=f9e290]:
- button "위로" [ref=f9e291]
- button "아래로" [disabled] [ref=f9e292]
- button "삭제" [ref=f9e293]
- button "선택지 추가" [ref=f9e294]
- generic [ref=f9e295]:
- generic [ref=f9e296]: 다음 검증
- textbox "다음 검증" [ref=f9e297]: 저장소를 공유한 뒤에 재현한다. 1. replica 두 대에서 같은 사용자로 access token 만료 직후 동시에 요청을 보낸다. 2. 이긴 쪽과 지는 쪽의 응답을 각각 기록한다. 3. 지는 쪽이 저장된 새 token으로 재시도해 성공하는지 본다. 4. 지는 쪽 사용자 화면에 무엇이 보이는지 기록한다. 5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다. 실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
- region [ref=f9e298]:
- generic [ref=f9e299]:
- paragraph [ref=f9e300]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f9e301]
- generic [ref=f9e304]:
- generic [ref=f9e305]:
- navigation "문서 경로" [ref=f9e306]:
- link "Open Question" [ref=f9e307] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f9e308]: /
- generic [ref=f9e309]: OAuth/OIDC 인증 경계
- generic [ref=f9e310]: /
- link "KeyCloak Patterns" [ref=f9e311] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [level=1] [ref=f9e312]
- paragraph [ref=f9e313]: realm이 refresh token rotation과 재사용 허용 0회를 쓴다. 두 replica가 같은 refresh token으로 동시에 갱신할 수 있고, 그때 두 번째 사용이 거부될 가능성이 있다. 실제 Keycloak 응답과 session 영향은 아직 재현하지 않았다.
- generic [ref=f9e314]:
- generic [ref=f9e315]:
- term [ref=f9e316]: 유형
- definition [ref=f9e317]: Open Question
- generic [ref=f9e318]:
- term [ref=f9e319]: 프로젝트
- definition [ref=f9e320]: KeyCloak Patterns
- generic [ref=f9e321]:
- term [ref=f9e322]: 게시
- definition [ref=f9e323]: 게시 전
- paragraph [ref=f9e324]: OPEN
- article [ref=f9e325]:
- region [ref=f9e326]:
- heading "확인한 사실" [level=2] [ref=f9e327]
- list [ref=f9e328]:
- listitem [ref=f9e329]: realm은 refresh token rotation과 재사용 허용 0회를 쓰게 되어서, 한 번 갱신하면 이전 refresh token은 바로 무효가 된다.
- listitem [ref=f9e330]: 커밋된 테스트는 새 refresh token 발급과 이전 token 거부, revocation 뒤 refresh 실패를 확인하는데 모두 한 주체가 순서대로 부르는 경우다.
- listitem [ref=f9e331]: authorized client manager에는 refresh-token provider가 구성되어 있어 access token 만료 시 refresh를 시도할 수 있다.
- listitem [ref=f9e332]: 다만 만료를 기다려 실제 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
- listitem [ref=f9e333]: 현재 authorized client 저장소는 process-local이라 replica가 같은 refresh token 상태를 공유하지 않는다. 따라서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
- listitem [ref=f9e334]: 이미 발급된 access token은 만료 전까지 API에서 계속 통하기 때문에, 갱신이 실패해도 그동안은 화면이 정상으로 보이게 된다.
- region [ref=f9e335]:
- heading "가정" [level=2] [ref=f9e336]
- list [ref=f9e337]:
- listitem [ref=f9e338]: 운영에서는 replica가 둘 이상이고 저장소를 공유해 같은 authorized client 항목을 보게 된다.
- listitem [ref=f9e339]: 두 replica가 비슷한 시각에 만료를 만나면 각각 갱신을 시도하게 된다.
- region [ref=f9e340]:
- heading "남은 미지수" [level=2] [ref=f9e341]
- list [ref=f9e342]:
- listitem [ref=f9e343]: 같은 refresh token으로 두 replica가 동시에 갱신하면 어느 쪽이 이기고 지는 쪽은 무엇을 받게 되는가.
- listitem [ref=f9e344]: 재사용 허용 0회에서 지는 쪽의 요청이 사용자 화면에 어떻게 보이게 되는가. 로그인 만료로 보이는가 일시적 오류로 보이는가.
- listitem [ref=f9e345]: 지는 쪽이 저장소에서 새 token을 다시 읽어 재시도하면 성공하게 되는가, 아니면 재인증이 필요해지는가.
- listitem [ref=f9e346]: 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
- listitem [ref=f9e347]: lock을 쓴다면 어디에 두고 얼마나 잡게 되는가. 잡은 채로 프로세스가 내려가면 어떻게 푸는가.
- listitem [ref=f9e348]: 갱신 실패를 로그인 만료와 구분해서 표시할 수 있게 되는가.
- region [ref=f9e349]:
- heading "제약" [level=2] [ref=f9e350]
- list [ref=f9e351]:
- listitem [ref=f9e352]: rotation과 재사용 0회는 이미 realm 설정이라서 이 전제를 바꾸지 않고 답해야 한다.
- listitem [ref=f9e353]: 이미 발급된 access token은 만료 전까지 사용할 수 있으므로 refresh 실패는 즉시 보이지 않을 수 있다. 재현 테스트는 access token 만료 직후에 맞춰 실행한다.
- listitem [ref=f9e354]: 이 경쟁은 저장소를 공유한 뒤에야 재현되기 때문에 저장소 결정이 이 질문보다 앞서게 된다.
- region [ref=f9e355]:
- heading "검토한 선택지" [level=2] [ref=f9e356]
- list [ref=f9e357]:
- listitem [ref=f9e358]:
- heading "분산 lock으로 갱신을 직렬화한다" [level=3] [ref=f9e359]
- paragraph [ref=f9e360]: 한 replica만 갱신하고 나머지는 끝나기를 기다렸다가 결과를 읽게 되어서 재사용 거부가 아예 생기지 않는다. 분산 lock을 사용하면 refresh 구간을 직렬화할 수 있다. lock 저장소의 가용성, lock 만료, 재진입, lock 보유 process 종료 상황까지 함께 처리해야 한다.
- listitem [ref=f9e361]:
- heading "각자 갱신하고 실패는 재시도로 처리한다" [level=3] [ref=f9e362]
- paragraph [ref=f9e363]: 구현이 가장 단순하다. 지는 쪽이 거부를 받으면 저장소에서 최신 token을 다시 읽어 재시도한다는 전제인데, 이 재시도가 성립하는지는 아직 확인하지 않았다. reuse detection 정책에 따라 같은 refresh token의 두 번째 사용이 token family 전체에 영향을 줄 수 있다. 이 경우 단순 retry로 끝나지 않고 재인증이 필요할 수 있다. 갱신에 성공한 replica가 새 token을 저장하기 전에 다른 replica가 다시 조회하는 순서도 별도로 재현해야 한다.
- listitem [ref=f9e364]:
- heading "갱신 전용 경로를 하나 둔다" [level=3] [ref=f9e365]
- paragraph [ref=f9e366]: refresh를 전담하는 구성요소 하나만 refresh token을 사용하고 다른 replica는 갱신 결과를 조회하도록 구성할 수 있다. refresh 전담 구성요소가 중단되면 access token 만료 이후 갱신을 수행할 주체가 없어지므로 해당 구성요소의 가용성과 복구 방식이 중요해진다.
- listitem [ref=f9e367]:
- heading "제약상 제외 — 재사용 허용을 늘린다" [level=3] [ref=f9e368]
- paragraph [ref=f9e369]: 짧은 유예를 주면 경쟁이 저절로 해소되고 코드도 고칠 필요가 없다. 다만 rotation과 재사용 0회는 이 질문이 바꾸지 않기로 한 realm 설정이다. 훔친 refresh token을 쓸 수 있는 창도 같이 늘어난다. 비교 대상으로만 남긴다.
- region [ref=f9e370]:
- paragraph [ref=f9e371]: Next
- heading "다음 검증" [level=2] [ref=f9e372]
- paragraph [ref=f9e373]: 저장소를 공유한 뒤에 재현한다.1. replica 두 대에서 같은 사용자로 access token 만료 직후 동시에 요청을 보낸다.2. 이긴 쪽과 지는 쪽의 응답을 각각 기록한다.3. 지는 쪽이 저장된 새 token으로 재시도해 성공하는지 본다.4. 지는 쪽 사용자 화면에 무엇이 보이는지 기록한다.5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다.실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
- region [ref=f9e374]:
- paragraph [ref=f9e375]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f9e376]
- list [ref=f9e377]:
- listitem [ref=f9e378]:
- link "rotation과 재사용 0회를 쓰는 구성의 출처다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f9e379] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f9e380]: rotation과 재사용 0회를 쓰는 구성의 출처다.
- strong [ref=f9e381]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f9e382]:
- complementary [ref=f9e383]:
- heading "작업 상태" [level=2] [ref=f9e384]
- status "편집 상태" [ref=f9e385]: 저장됨
- generic [ref=f9e386]:
- generic [ref=f9e387]:
- term [ref=f9e388]: 저장 버전
- definition [ref=f9e389]: "11"
- generic [ref=f9e390]:
- term [ref=f9e391]: 종류
- definition [ref=f9e392]: QUESTION
- paragraph [ref=f9e393]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f9e394]:
- button "저장" [disabled] [ref=f9e395]
- button "게시" [ref=f9e396]
- paragraph [ref=f9e397]: 버전 11으로 저장했습니다.
@@ -0,0 +1,509 @@
- generic [ref=f10e3]:
- link "본문으로 건너뛰기" [ref=f10e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f10e5]:
- generic [ref=f10e6]:
- link "TechLog Studio" [ref=f10e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f10e8]: Studio
- navigation "Studio 주 탐색" [ref=f10e10]:
- link "작업본" [ref=f10e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f10e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f10e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f10e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f10e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f10e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f10e17]
- main [ref=f10e18]:
- generic [ref=f10e19]:
- generic [ref=f10e20]:
- region [ref=f10e21]:
- generic [ref=f10e22]:
- paragraph [ref=f10e23]: QUESTION · VERSION 11
- heading "문서 편집" [level=1] [ref=f10e24]
- paragraph [ref=f10e25]: 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가
- region [ref=f10e26]:
- generic [ref=f10e27]:
- paragraph [ref=f10e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f10e29]
- generic [ref=f10e30]:
- generic [ref=f10e31]:
- generic [ref=f10e32]: 제목
- textbox "제목" [ref=f10e33]: 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가
- generic [ref=f10e34]:
- generic [ref=f10e35]: slug
- textbox "slug" [ref=f10e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: server-session-pattern-multi-instance
- generic [ref=f10e37]:
- generic [ref=f10e38]: 요약
- textbox "요약" [ref=f10e39]: Mediator와 BFF는 로그인 상태와 token 상태를 열쇠가 다른 두 저장소에 나눠 두게 되는데 지금은 두 상태가 다 process 안에 있다. 인스턴스가 둘 이상인 운영에서 재시작과 이동, logout이 어떻게 동작해야 하는지 아직 정하지 않았다.
- generic [ref=f10e40]:
- generic [ref=f10e41]: Topic
- combobox "Topic" [ref=f10e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f10e43]:
- generic [ref=f10e44]: Project
- combobox "Project" [ref=f10e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f10e46]:
- generic [ref=f10e48]:
- generic [ref=f10e49]:
- generic [ref=f10e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f10e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f10e52]:
- generic [ref=f10e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f10e54]: 두 상태가 모두 process-local memory에 있다는 사실의 출처다.
- generic [ref=f10e55]:
- button "위로" [disabled] [ref=f10e56]
- button "아래로" [ref=f10e57]
- button "삭제" [ref=f10e58]
- generic [ref=f10e59]:
- generic [ref=f10e60]:
- generic [ref=f10e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f10e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f10e63]:
- generic [ref=f10e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f10e65]: 같은 저장소 구성을 쓰는 다른 패턴이다.
- generic [ref=f10e66]:
- button "위로" [ref=f10e67]
- button "아래로" [ref=f10e68]
- button "삭제" [ref=f10e69]
- generic [ref=f10e70]:
- generic [ref=f10e71]:
- generic [ref=f10e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f10e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f10e74]:
- generic [ref=f10e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f10e76]: 이 질문의 답이 이 기준의 빈 항목을 채운다.
- generic [ref=f10e77]:
- button "위로" [ref=f10e78]
- button "아래로" [ref=f10e79]
- button "삭제" [ref=f10e80]
- generic [ref=f10e81]:
- generic [ref=f10e82]:
- generic [ref=f10e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f10e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f10e85]:
- generic [ref=f10e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f10e87]: 저장소 후보 비교로 독립시킨 질문이다.
- generic [ref=f10e88]:
- button "위로" [ref=f10e89]
- button "아래로" [disabled] [ref=f10e90]
- button "삭제" [ref=f10e91]
- button "관계 추가" [ref=f10e92]
- region [ref=f10e93]:
- generic [ref=f10e94]:
- paragraph [ref=f10e95]: QUESTION
- heading "판단과 다음 검증" [level=2] [ref=f10e96]
- generic [ref=f10e97]:
- generic [ref=f10e98]: 질문 상태
- combobox "질문 상태" [ref=f10e99]:
- option "아직 정하지 않음"
- option "OPEN" [selected]
- option "RESOLVED"
- group "사실" [ref=f10e100]:
- generic [ref=f10e102]:
- generic [ref=f10e103]:
- generic [ref=f10e104]: 사실 1
- textbox "사실 1" [ref=f10e105]: Mediator와 BFF는 로그인 상태를 HttpSession에 두고 token은 OAuth2AuthorizedClientService에 두게 되는데, 두 저장소는 열쇠가 다르다. session은 session ID로 찾고 authorized client는 registration 이름과 principal name으로 찾는다.
- generic [ref=f10e106]:
- button "위로" [disabled] [ref=f10e107]
- button "아래로" [ref=f10e108]
- button "삭제" [ref=f10e109]
- generic [ref=f10e110]:
- generic [ref=f10e111]:
- generic [ref=f10e112]: 사실 2
- textbox "사실 2" [ref=f10e113]: 현재 두 저장소는 Spring Boot 자동구성이 선택한 in-memory 구현을 사용한다. 코드에서 store bean을 직접 선언하지 않았기 때문에 실제 구현은 자동구성 결과를 함께 확인해야 한다.
- generic [ref=f10e114]:
- button "위로" [ref=f10e115]
- button "아래로" [ref=f10e116]
- button "삭제" [ref=f10e117]
- generic [ref=f10e118]:
- generic [ref=f10e119]:
- generic [ref=f10e120]: 사실 3
- textbox "사실 3" [ref=f10e121]: Spring Session과 Redis, JDBC token store 의존성이 없어서 두 상태가 모두 process 안에 있다. 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client는 사라진다.
- generic [ref=f10e122]:
- button "위로" [ref=f10e123]
- button "아래로" [ref=f10e124]
- button "삭제" [ref=f10e125]
- generic [ref=f10e126]:
- generic [ref=f10e127]:
- generic [ref=f10e128]: 사실 4
- textbox "사실 4" [ref=f10e129]: authorized client의 열쇠에 session ID가 없기 때문에 같은 사용자가 두 브라우저에서 로그인하면 같은 항목을 보게 된다.
- generic [ref=f10e130]:
- button "위로" [ref=f10e131]
- button "아래로" [ref=f10e132]
- button "삭제" [ref=f10e133]
- generic [ref=f10e134]:
- generic [ref=f10e135]:
- generic [ref=f10e136]: 사실 5
- textbox "사실 5" [ref=f10e137]: OAuth2-Proxy 구조는 server-side session store를 두지 않고 최소 정보만 담은 client-side cookie를 쓰게 되며, cookie 만료는 proxy 설정의 1 hour다.
- generic [ref=f10e138]:
- button "위로" [ref=f10e139]
- button "아래로" [ref=f10e140]
- button "삭제" [ref=f10e141]
- generic [ref=f10e142]:
- generic [ref=f10e143]:
- generic [ref=f10e144]: 사실 6
- textbox "사실 6" [ref=f10e145]: 커밋된 테스트에 재시작이나 replica 이동 뒤 복구 계약이 없어서 지금 무엇을 바꿔도 회귀를 잡아 줄 검사가 없다.
- generic [ref=f10e146]:
- button "위로" [ref=f10e147]
- button "아래로" [disabled] [ref=f10e148]
- button "삭제" [ref=f10e149]
- button "사실 추가" [ref=f10e150]
- group "가정" [ref=f10e151]:
- generic [ref=f10e153]:
- generic [ref=f10e154]:
- generic [ref=f10e155]: 가정 1
- textbox "가정 1" [ref=f10e156]: 운영에서는 인스턴스가 둘 이상이다.
- generic [ref=f10e157]:
- button "위로" [disabled] [ref=f10e158]
- button "아래로" [ref=f10e159]
- button "삭제" [ref=f10e160]
- generic [ref=f10e161]:
- generic [ref=f10e162]:
- generic [ref=f10e163]: 가정 2
- textbox "가정 2" [ref=f10e164]: 재시작과 배포가 로그인 상태를 끊어서는 안 되는데, 지금 구조에서는 끊기게 된다.
- generic [ref=f10e165]:
- button "위로" [ref=f10e166]
- button "아래로" [ref=f10e167]
- button "삭제" [ref=f10e168]
- generic [ref=f10e169]:
- generic [ref=f10e170]:
- generic [ref=f10e171]: 가정 3
- textbox "가정 3" [ref=f10e172]: 같은 사용자의 여러 브라우저 session이 서로의 token 항목을 덮어써서는 안 된다.
- generic [ref=f10e173]:
- button "위로" [ref=f10e174]
- button "아래로" [disabled] [ref=f10e175]
- button "삭제" [ref=f10e176]
- button "가정 추가" [ref=f10e177]
- group "미지수" [ref=f10e178]:
- generic [ref=f10e180]:
- generic [ref=f10e181]:
- generic [ref=f10e182]: 미지수 1
- textbox "미지수 1" [ref=f10e183]: 재시작 뒤 로그인이 유지되는가. 지금은 안 된다는 것까지 알지만 무엇을 바꿔야 되는지는 정하지 않았다.
- generic [ref=f10e184]:
- button "위로" [disabled] [ref=f10e185]
- button "아래로" [ref=f10e186]
- button "삭제" [ref=f10e187]
- generic [ref=f10e188]:
- generic [ref=f10e189]:
- generic [ref=f10e190]: 미지수 2
- textbox "미지수 2" [ref=f10e191]: 인스턴스가 바뀌어도 같은 session을 찾게 되는가.
- generic [ref=f10e192]:
- button "위로" [ref=f10e193]
- button "아래로" [ref=f10e194]
- button "삭제" [ref=f10e195]
- generic [ref=f10e196]:
- generic [ref=f10e197]:
- generic [ref=f10e198]: 미지수 3
- textbox "미지수 3" [ref=f10e199]: 같은 사용자의 여러 session이 authorized client 항목을 공유하거나 덮어쓰게 되는가. 한쪽에서 로그아웃하면 다른 쪽도 끊기게 되는가.
- generic [ref=f10e200]:
- button "위로" [ref=f10e201]
- button "아래로" [ref=f10e202]
- button "삭제" [ref=f10e203]
- generic [ref=f10e204]:
- generic [ref=f10e205]:
- generic [ref=f10e206]: 미지수 4
- textbox "미지수 4" [ref=f10e207]: 저장된 refresh token이 암호화되는가. 저장소를 여는 사람이 그 값을 그대로 읽게 되는가.
- generic [ref=f10e208]:
- button "위로" [ref=f10e209]
- button "아래로" [ref=f10e210]
- button "삭제" [ref=f10e211]
- generic [ref=f10e212]:
- generic [ref=f10e213]:
- generic [ref=f10e214]: 미지수 5
- textbox "미지수 5" [ref=f10e215]: logout에서 HttpSession과 authorized client를 모두 정리하는가. 한쪽만 삭제했을 때 다음 요청이나 재로그인에서 어떤 상태가 복원되는가.
- generic [ref=f10e216]:
- button "위로" [ref=f10e217]
- button "아래로" [ref=f10e218]
- button "삭제" [ref=f10e219]
- generic [ref=f10e220]:
- generic [ref=f10e221]:
- generic [ref=f10e222]: 미지수 6
- textbox "미지수 6" [ref=f10e223]: session 만료와 token 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 어떻게 보이게 되는가.
- generic [ref=f10e224]:
- button "위로" [ref=f10e225]
- button "아래로" [ref=f10e226]
- button "삭제" [ref=f10e227]
- generic [ref=f10e228]:
- generic [ref=f10e229]:
- generic [ref=f10e230]: 미지수 7
- textbox "미지수 7" [ref=f10e231]: OAuth2-Proxy 구조의 replica들이 같은 cookie secret을 어떻게 공유하고 교체하게 되는가. 교체하는 동안 로그인해 있던 사람은 어떻게 되는가.
- generic [ref=f10e232]:
- button "위로" [ref=f10e233]
- button "아래로" [disabled] [ref=f10e234]
- button "삭제" [ref=f10e235]
- button "미지수 추가" [ref=f10e236]
- group "제약" [ref=f10e237]:
- generic [ref=f10e239]:
- generic [ref=f10e240]:
- generic [ref=f10e241]: 제약 1
- textbox "제약 1" [ref=f10e242]: 현재 예제는 단일 인스턴스로 실행하고 있어 replica 간 session 조회와 failover 동작은 아직 재현하지 않았다.
- generic [ref=f10e243]:
- button "위로" [disabled] [ref=f10e244]
- button "아래로" [ref=f10e245]
- button "삭제" [ref=f10e246]
- generic [ref=f10e247]:
- generic [ref=f10e248]:
- generic [ref=f10e249]: 제약 2
- textbox "제약 2" [ref=f10e250]: authorized client의 key에는 session ID가 없다. session store를 shared store로 바꾸는 작업과 authorized client 저장 방식을 정하는 작업은 별도로 필요하다.
- generic [ref=f10e251]:
- button "위로" [ref=f10e252]
- button "아래로" [ref=f10e253]
- button "삭제" [ref=f10e254]
- generic [ref=f10e255]:
- generic [ref=f10e256]:
- generic [ref=f10e257]: 제약 3
- textbox "제약 3" [ref=f10e258]: Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 network에서 강제된 상태가 아니다.
- generic [ref=f10e259]:
- button "위로" [ref=f10e260]
- button "아래로" [disabled] [ref=f10e261]
- button "삭제" [ref=f10e262]
- button "제약 추가" [ref=f10e263]
- group "선택지" [ref=f10e264]:
- generic [ref=f10e266]:
- generic [ref=f10e267]:
- generic [ref=f10e268]: 선택지 1 제목
- textbox "선택지 1 제목" [ref=f10e269]: 공유 저장소를 사용한다
- generic [ref=f10e270]:
- generic [ref=f10e271]: 선택지 1 설명
- textbox "선택지 1 설명" [ref=f10e272]: HttpSession과 authorized client를 모두 외부 store에 두게 되면 인스턴스가 늘어도 같은 상태를 찾고 재시작도 견디게 된다. 공유 저장소를 사용하면 replica가 같은 상태를 조회할 수 있다. 반면 인증 경로가 저장소 가용성에 의존하므로 장애 처리, 직렬화 형식, token 암호화, session과 token의 만료 정합을 함께 설계해야 한다.
- generic [ref=f10e273]:
- button "위로" [disabled] [ref=f10e274]
- button "아래로" [ref=f10e275]
- button "삭제" [ref=f10e276]
- generic [ref=f10e277]:
- generic [ref=f10e278]:
- generic [ref=f10e279]: 선택지 2 제목
- textbox "선택지 2 제목" [ref=f10e280]: session affinity로 묶는다
- generic [ref=f10e281]:
- generic [ref=f10e282]: 선택지 2 설명
- textbox "선택지 2 설명" [ref=f10e283]: 같은 사용자를 같은 인스턴스로 보내게 되어서 코드를 거의 안 고쳐도 되고 저장소도 늘지 않는다. sticky session은 평상시 요청을 같은 인스턴스로 보낼 수 있지만 해당 인스턴스가 종료되면 process-local 상태도 함께 사용할 수 없게 된다. 배포나 오토스케일링처럼 인스턴스 교체가 잦은 환경에서는 별도 복구 전략이 필요하다.
- generic [ref=f10e284]:
- button "위로" [ref=f10e285]
- button "아래로" [ref=f10e286]
- button "삭제" [ref=f10e287]
- generic [ref=f10e288]:
- generic [ref=f10e289]:
- generic [ref=f10e290]: 선택지 3 제목
- textbox "선택지 3 제목" [ref=f10e291]: 브라우저가 token을 들고 API를 직접 부르게 되돌린다
- generic [ref=f10e292]:
- generic [ref=f10e293]: 선택지 3 설명
- textbox "선택지 3 설명" [ref=f10e294]: server에 상태를 두지 않게 되어서 공유 저장소도 affinity도 필요 없어지고 Resource Server는 요청마다 서명만 검증한다. SPA처럼 browser token을 사용하는 구조로 바꾸는 방법도 있지만, 브라우저에 OAuth token을 전달하지 않는 정책이 있다면 후보에서 제외한다.
- generic [ref=f10e295]:
- button "위로" [ref=f10e296]
- button "아래로" [ref=f10e297]
- button "삭제" [ref=f10e298]
- generic [ref=f10e299]:
- generic [ref=f10e300]:
- generic [ref=f10e301]: 선택지 4 제목
- textbox "선택지 4 제목" [ref=f10e302]: 저장소 선택이 아니라 구조 변경 — 최소 정보만 담은 client-side cookie
- generic [ref=f10e303]:
- generic [ref=f10e304]: 선택지 4 설명
- textbox "선택지 4 설명" [ref=f10e305]: 이것은 저장소를 바꾸는 선택이 아니다. server-side store를 없애고 인증 상태를 cookie 자체에 담는 구조 변경이라서 앞의 세 후보와 같은 층에 놓고 비교할 수 없다. Forward-Auth로 전환하면 애플리케이션이 server-side OAuth token store를 운영하지 않아도 된다. 이 구조에서는 replica가 공유할 cookie secret과 edge identity header를 신뢰하기 위한 network·header 검증을 운영해야 한다.
- generic [ref=f10e306]:
- button "위로" [ref=f10e307]
- button "아래로" [disabled] [ref=f10e308]
- button "삭제" [ref=f10e309]
- button "선택지 추가" [ref=f10e310]
- generic [ref=f10e311]:
- generic [ref=f10e312]: 다음 검증
- textbox "다음 검증" [ref=f10e313]: 인스턴스를 둘로 띄우고 순서대로 확인한다. 1. 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 유지되는지 본다. 2. 한 인스턴스를 재시작하고 같은 session cookie로 로그인 상태가 남는지 본다. 3. 같은 사용자로 두 브라우저에서 로그인해 authorized client 항목이 서로를 덮어쓰는지 본다. 4. 한쪽에서 logout한 뒤 다른 쪽 요청이 어떻게 되는지 본다. 5. session 만료를 token 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다. 여기서 무엇이 깨지는지가 갈리게 되면 저장소 후보 비교로 넘어간다.
- region [ref=f10e314]:
- generic [ref=f10e315]:
- paragraph [ref=f10e316]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f10e317]
- generic [ref=f10e320]:
- generic [ref=f10e321]:
- navigation "문서 경로" [ref=f10e322]:
- link "Open Question" [ref=f10e323] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f10e324]: /
- generic [ref=f10e325]: OAuth/OIDC 인증 경계
- generic [ref=f10e326]: /
- link "KeyCloak Patterns" [ref=f10e327] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [level=1] [ref=f10e328]
- paragraph [ref=f10e329]: Mediator와 BFF는 로그인 상태와 token 상태를 열쇠가 다른 두 저장소에 나눠 두게 되는데 지금은 두 상태가 다 process 안에 있다. 인스턴스가 둘 이상인 운영에서 재시작과 이동, logout이 어떻게 동작해야 하는지 아직 정하지 않았다.
- generic [ref=f10e330]:
- generic [ref=f10e331]:
- term [ref=f10e332]: 유형
- definition [ref=f10e333]: Open Question
- generic [ref=f10e334]:
- term [ref=f10e335]: 프로젝트
- definition [ref=f10e336]: KeyCloak Patterns
- generic [ref=f10e337]:
- term [ref=f10e338]: 게시
- definition [ref=f10e339]: 게시 전
- paragraph [ref=f10e340]: OPEN
- article [ref=f10e341]:
- region [ref=f10e342]:
- heading "확인한 사실" [level=2] [ref=f10e343]
- list [ref=f10e344]:
- listitem [ref=f10e345]: Mediator와 BFF는 로그인 상태를 HttpSession에 두고 token은 OAuth2AuthorizedClientService에 두게 되는데, 두 저장소는 열쇠가 다르다. session은 session ID로 찾고 authorized client는 registration 이름과 principal name으로 찾는다.
- listitem [ref=f10e346]: 현재 두 저장소는 Spring Boot 자동구성이 선택한 in-memory 구현을 사용한다. 코드에서 store bean을 직접 선언하지 않았기 때문에 실제 구현은 자동구성 결과를 함께 확인해야 한다.
- listitem [ref=f10e347]: Spring Session과 Redis, JDBC token store 의존성이 없어서 두 상태가 모두 process 안에 있다. 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client는 사라진다.
- listitem [ref=f10e348]: authorized client의 열쇠에 session ID가 없기 때문에 같은 사용자가 두 브라우저에서 로그인하면 같은 항목을 보게 된다.
- listitem [ref=f10e349]: OAuth2-Proxy 구조는 server-side session store를 두지 않고 최소 정보만 담은 client-side cookie를 쓰게 되며, cookie 만료는 proxy 설정의 1 hour다.
- listitem [ref=f10e350]: 커밋된 테스트에 재시작이나 replica 이동 뒤 복구 계약이 없어서 지금 무엇을 바꿔도 회귀를 잡아 줄 검사가 없다.
- region [ref=f10e351]:
- heading "가정" [level=2] [ref=f10e352]
- list [ref=f10e353]:
- listitem [ref=f10e354]: 운영에서는 인스턴스가 둘 이상이다.
- listitem [ref=f10e355]: 재시작과 배포가 로그인 상태를 끊어서는 안 되는데, 지금 구조에서는 끊기게 된다.
- listitem [ref=f10e356]: 같은 사용자의 여러 브라우저 session이 서로의 token 항목을 덮어써서는 안 된다.
- region [ref=f10e357]:
- heading "남은 미지수" [level=2] [ref=f10e358]
- list [ref=f10e359]:
- listitem [ref=f10e360]: 재시작 뒤 로그인이 유지되는가. 지금은 안 된다는 것까지 알지만 무엇을 바꿔야 되는지는 정하지 않았다.
- listitem [ref=f10e361]: 인스턴스가 바뀌어도 같은 session을 찾게 되는가.
- listitem [ref=f10e362]: 같은 사용자의 여러 session이 authorized client 항목을 공유하거나 덮어쓰게 되는가. 한쪽에서 로그아웃하면 다른 쪽도 끊기게 되는가.
- listitem [ref=f10e363]: 저장된 refresh token이 암호화되는가. 저장소를 여는 사람이 그 값을 그대로 읽게 되는가.
- listitem [ref=f10e364]: logout에서 HttpSession과 authorized client를 모두 정리하는가. 한쪽만 삭제했을 때 다음 요청이나 재로그인에서 어떤 상태가 복원되는가.
- listitem [ref=f10e365]: session 만료와 token 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 어떻게 보이게 되는가.
- listitem [ref=f10e366]: OAuth2-Proxy 구조의 replica들이 같은 cookie secret을 어떻게 공유하고 교체하게 되는가. 교체하는 동안 로그인해 있던 사람은 어떻게 되는가.
- region [ref=f10e367]:
- heading "제약" [level=2] [ref=f10e368]
- list [ref=f10e369]:
- listitem [ref=f10e370]: 현재 예제는 단일 인스턴스로 실행하고 있어 replica 간 session 조회와 failover 동작은 아직 재현하지 않았다.
- listitem [ref=f10e371]: authorized client의 key에는 session ID가 없다. session store를 shared store로 바꾸는 작업과 authorized client 저장 방식을 정하는 작업은 별도로 필요하다.
- listitem [ref=f10e372]: Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 network에서 강제된 상태가 아니다.
- region [ref=f10e373]:
- heading "검토한 선택지" [level=2] [ref=f10e374]
- list [ref=f10e375]:
- listitem [ref=f10e376]:
- heading "공유 저장소를 사용한다" [level=3] [ref=f10e377]
- paragraph [ref=f10e378]: HttpSession과 authorized client를 모두 외부 store에 두게 되면 인스턴스가 늘어도 같은 상태를 찾고 재시작도 견디게 된다. 공유 저장소를 사용하면 replica가 같은 상태를 조회할 수 있다. 반면 인증 경로가 저장소 가용성에 의존하므로 장애 처리, 직렬화 형식, token 암호화, session과 token의 만료 정합을 함께 설계해야 한다.
- listitem [ref=f10e379]:
- heading "session affinity로 묶는다" [level=3] [ref=f10e380]
- paragraph [ref=f10e381]: 같은 사용자를 같은 인스턴스로 보내게 되어서 코드를 거의 안 고쳐도 되고 저장소도 늘지 않는다. sticky session은 평상시 요청을 같은 인스턴스로 보낼 수 있지만 해당 인스턴스가 종료되면 process-local 상태도 함께 사용할 수 없게 된다. 배포나 오토스케일링처럼 인스턴스 교체가 잦은 환경에서는 별도 복구 전략이 필요하다.
- listitem [ref=f10e382]:
- heading "브라우저가 token을 들고 API를 직접 부르게 되돌린다" [level=3] [ref=f10e383]
- paragraph [ref=f10e384]: server에 상태를 두지 않게 되어서 공유 저장소도 affinity도 필요 없어지고 Resource Server는 요청마다 서명만 검증한다. SPA처럼 browser token을 사용하는 구조로 바꾸는 방법도 있지만, 브라우저에 OAuth token을 전달하지 않는 정책이 있다면 후보에서 제외한다.
- listitem [ref=f10e385]:
- heading "저장소 선택이 아니라 구조 변경 — 최소 정보만 담은 client-side cookie" [level=3] [ref=f10e386]
- paragraph [ref=f10e387]: 이것은 저장소를 바꾸는 선택이 아니다. server-side store를 없애고 인증 상태를 cookie 자체에 담는 구조 변경이라서 앞의 세 후보와 같은 층에 놓고 비교할 수 없다. Forward-Auth로 전환하면 애플리케이션이 server-side OAuth token store를 운영하지 않아도 된다. 이 구조에서는 replica가 공유할 cookie secret과 edge identity header를 신뢰하기 위한 network·header 검증을 운영해야 한다.
- region [ref=f10e388]:
- paragraph [ref=f10e389]: Next
- heading "다음 검증" [level=2] [ref=f10e390]
- paragraph [ref=f10e391]: 인스턴스를 둘로 띄우고 순서대로 확인한다.1. 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 유지되는지 본다.2. 한 인스턴스를 재시작하고 같은 session cookie로 로그인 상태가 남는지 본다.3. 같은 사용자로 두 브라우저에서 로그인해 authorized client 항목이 서로를 덮어쓰는지 본다.4. 한쪽에서 logout한 뒤 다른 쪽 요청이 어떻게 되는지 본다.5. session 만료를 token 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다.여기서 무엇이 깨지는지가 갈리게 되면 저장소 후보 비교로 넘어간다.
- region [ref=f10e392]:
- paragraph [ref=f10e393]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f10e394]
- list [ref=f10e395]:
- listitem [ref=f10e396]:
- link "두 상태가 모두 process-local memory에 있다는 사실의 출처다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f10e397] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f10e398]: 두 상태가 모두 process-local memory에 있다는 사실의 출처다.
- strong [ref=f10e399]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f10e400]:
- listitem [ref=f10e401]:
- link "같은 저장소 구성을 쓰는 다른 패턴이다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f10e402] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f10e403]: 같은 저장소 구성을 쓰는 다른 패턴이다.
- strong [ref=f10e404]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f10e405]:
- complementary [ref=f10e406]:
- heading "작업 상태" [level=2] [ref=f10e407]
- status "편집 상태" [ref=f10e408]: 저장됨
- generic [ref=f10e409]:
- generic [ref=f10e410]:
- term [ref=f10e411]: 저장 버전
- definition [ref=f10e412]: "11"
- generic [ref=f10e413]:
- term [ref=f10e414]: 종류
- definition [ref=f10e415]: QUESTION
- paragraph [ref=f10e416]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f10e417]:
- button "저장" [disabled] [ref=f10e418]
- button "게시" [ref=f10e419]
- paragraph [ref=f10e420]: 버전 11으로 저장했습니다.
@@ -0,0 +1,429 @@
- generic [ref=f11e3]:
- link "본문으로 건너뛰기" [ref=f11e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f11e5]:
- generic [ref=f11e6]:
- link "TechLog Studio" [ref=f11e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f11e8]: Studio
- navigation "Studio 주 탐색" [ref=f11e10]:
- link "작업본" [ref=f11e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f11e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f11e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f11e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f11e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f11e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f11e17]
- main [ref=f11e18]:
- generic [ref=f11e19]:
- generic [ref=f11e20]:
- region [ref=f11e21]:
- generic [ref=f11e22]:
- paragraph [ref=f11e23]: REFERENCE · VERSION 13
- heading "문서 편집" [level=1] [ref=f11e24]
- paragraph [ref=f11e25]: Public Client와 Confidential Client 구분 기준
- region [ref=f11e26]:
- generic [ref=f11e27]:
- paragraph [ref=f11e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f11e29]
- generic [ref=f11e30]:
- generic [ref=f11e31]:
- generic [ref=f11e32]: 제목
- textbox "제목" [ref=f11e33]: Public Client와 Confidential Client 구분 기준
- generic [ref=f11e34]:
- generic [ref=f11e35]: slug
- textbox "slug" [ref=f11e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: public-confidential-client-boundary
- generic [ref=f11e37]:
- generic [ref=f11e38]: 요약
- textbox "요약" [ref=f11e39]: client 종류는 secret을 안전하게 보관할 수 있는지로 정한다. SPA는 보관할 곳이 없어 public client로 등록한다. 종류는 secret이 어디 있는지를 말할 뿐이고, 브라우저에 token이 가는지는 따로 정해진다.
- generic [ref=f11e40]:
- generic [ref=f11e41]: Topic
- combobox "Topic" [ref=f11e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f11e43]:
- generic [ref=f11e44]: Project
- combobox "Project" [ref=f11e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f11e46]:
- generic [ref=f11e48]:
- generic [ref=f11e49]:
- generic [ref=f11e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f11e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [disabled]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f11e52]:
- generic [ref=f11e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f11e54]: SPA를 public client로 등록한 이유를 실제 구성에서 확인할 수 있다.
- generic [ref=f11e55]:
- button "위로" [disabled] [ref=f11e56]
- button "아래로" [ref=f11e57]
- button "삭제" [ref=f11e58]
- generic [ref=f11e59]:
- generic [ref=f11e60]:
- generic [ref=f11e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f11e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [disabled]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f11e63]:
- generic [ref=f11e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f11e65]: confidential client를 사용해도 access token 전달 방식은 별도로 설계된다는 예다.
- generic [ref=f11e66]:
- button "위로" [ref=f11e67]
- button "아래로" [ref=f11e68]
- button "삭제" [ref=f11e69]
- generic [ref=f11e70]:
- generic [ref=f11e71]:
- generic [ref=f11e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f11e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [selected]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f11e74]:
- generic [ref=f11e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f11e76]: client 종류에 따라 token endpoint의 client 인증 방식이 달라진다.
- generic [ref=f11e77]:
- button "위로" [ref=f11e78]
- button "아래로" [disabled] [ref=f11e79]
- button "삭제" [ref=f11e80]
- button "관계 추가" [ref=f11e81]
- region [ref=f11e82]:
- generic [ref=f11e83]:
- paragraph [ref=f11e84]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f11e85]
- generic [ref=f11e86]:
- generic [ref=f11e87]: 목적
- textbox "목적" [ref=f11e88]: client 종류를 무엇으로 정하는지부터 맞춰야 PKCE와 client 인증을 어디에 둘지 정할 수 있게 된다. 기준은 프레임워크나 언어가 아니라 값이 도달하는 범위다. 브라우저에서 실행되는 코드에 넣은 값은 개발자 도구를 열면 그대로 보이기 때문에 SPA는 secret을 가질 수 없고, server와 BFF는 그 값을 process 밖으로 내보내지 않을 수 있어서 secret을 들고 있게 된다. 여기서 자주 섞이는 것이 하나 있는데, 종류가 confidential이어도 브라우저에 token이 갈 수 있다. 서로 다른 결정이라서 따로 답해야 한다.
- group "규칙" [ref=f11e89]:
- generic [ref=f11e91]:
- generic [ref=f11e92]:
- generic [ref=f11e93]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f11e94]: secret을 숨길 수 있는지로 종류를 정한다
- generic [ref=f11e95]:
- generic [ref=f11e96]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f11e97]: 배포물이나 실행 중 memory에서 사용자가 값을 꺼낼 수 있으면 public client가 되고, server 안에만 두고 응답으로 나가지 않게 할 수 있으면 confidential client다. native app은 브라우저가 아니지만 배포물을 뜯으면 값이 나오기 때문에 여기서도 public client로 다루게 된다. 실행 환경의 이름이 아니라 값이 어디까지 가는지로 정한다.
- generic [ref=f11e98]:
- button "위로" [disabled] [ref=f11e99]
- button "아래로" [ref=f11e100]
- button "삭제" [ref=f11e101]
- generic [ref=f11e102]:
- generic [ref=f11e103]:
- generic [ref=f11e104]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f11e105]: public client에서도 Authorization Code Flow에 PKCE를 함께 쓴다
- generic [ref=f11e106]:
- generic [ref=f11e107]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f11e108]: PKCE는 client secret을 대체하는 client 인증 방식이 아니다. authorization request에서 만든 verifier와 token request의 verifier를 연결해 탈취된 authorization code의 교환을 어렵게 만든다. 여기서 S256을 쓴다. plain은 challenge가 verifier 그대로라서 중간에서 본 사람이 그대로 쓸 수 있다.
- generic [ref=f11e109]:
- button "위로" [ref=f11e110]
- button "아래로" [ref=f11e111]
- button "삭제" [ref=f11e112]
- generic [ref=f11e113]:
- generic [ref=f11e114]:
- generic [ref=f11e115]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f11e116]: confidential client에도 PKCE를 함께 쓸 수 있다
- generic [ref=f11e117]:
- generic [ref=f11e118]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f11e119]: client 인증이 있어도 PKCE는 여전히 쓸모가 있다. 두 장치가 막는 구간이 서로 달라서 함께 두면 그만큼 좁아지게 된다. 다만 「Authorization Code를 쓴다」와 「PKCE S256까지 설정으로 고정했다」는 서로 다른 주장이다. 설정과 테스트에서 확인한 범위까지만 말할 수 있다.
- generic [ref=f11e120]:
- button "위로" [ref=f11e121]
- button "아래로" [ref=f11e122]
- button "삭제" [ref=f11e123]
- generic [ref=f11e124]:
- generic [ref=f11e125]:
- generic [ref=f11e126]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f11e127]: public client에서는 implicit flow와 direct access grant를 끈다
- generic [ref=f11e128]:
- generic [ref=f11e129]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f11e130]: implicit flow는 token을 redirect fragment로 받게 되어서 주소창과 히스토리에 token이 남고, direct access grant는 애플리케이션이 사용자의 아이디와 비밀번호를 직접 받게 되어서 IdP만 알면 되는 값을 애플리케이션이 만지게 된다. 현재 예제에서는 Authorization Code Flow를 사용하므로 implicit flow와 direct access grant를 비활성화했다.
- generic [ref=f11e131]:
- button "위로" [ref=f11e132]
- button "아래로" [ref=f11e133]
- button "삭제" [ref=f11e134]
- generic [ref=f11e135]:
- generic [ref=f11e136]:
- generic [ref=f11e137]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f11e138]: 종류가 곧 브라우저 token 유무는 아니다
- generic [ref=f11e139]:
- generic [ref=f11e140]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f11e141]: confidential client가 code를 교환해도 그 결과인 access token을 응답 본문으로 브라우저에 건넬 수 있고, 실제로 그렇게 도는 구조가 있다. 종류는 secret을 어디에 두는지를 말하고, token 노출은 어느 계층이 API를 부르는지에 따라 갈린다.
- generic [ref=f11e142]:
- button "위로" [ref=f11e143]
- button "아래로" [disabled] [ref=f11e144]
- button "삭제" [ref=f11e145]
- button "규칙 추가" [ref=f11e146]
- group "적용 조건" [ref=f11e147]:
- generic [ref=f11e149]:
- generic [ref=f11e150]:
- generic [ref=f11e151]: 적용 조건 1
- textbox "적용 조건 1" [ref=f11e152]: 새 OAuth client를 등록할 때
- generic [ref=f11e153]:
- button "위로" [disabled] [ref=f11e154]
- button "아래로" [ref=f11e155]
- button "삭제" [ref=f11e156]
- generic [ref=f11e157]:
- generic [ref=f11e158]:
- generic [ref=f11e159]: 적용 조건 2
- textbox "적용 조건 2" [ref=f11e160]: SPA와 server 중 어디가 code를 교환할지 정할 때
- generic [ref=f11e161]:
- button "위로" [ref=f11e162]
- button "아래로" [ref=f11e163]
- button "삭제" [ref=f11e164]
- generic [ref=f11e165]:
- generic [ref=f11e166]:
- generic [ref=f11e167]: 적용 조건 3
- textbox "적용 조건 3" [ref=f11e168]: PKCE와 client 인증을 어디에 둘지 정할 때
- generic [ref=f11e169]:
- button "위로" [ref=f11e170]
- button "아래로" [ref=f11e171]
- button "삭제" [ref=f11e172]
- generic [ref=f11e173]:
- generic [ref=f11e174]:
- generic [ref=f11e175]: 적용 조건 4
- textbox "적용 조건 4" [ref=f11e176]: 기존 client의 종류가 맞는지 다시 볼 때
- generic [ref=f11e177]:
- button "위로" [ref=f11e178]
- button "아래로" [disabled] [ref=f11e179]
- button "삭제" [ref=f11e180]
- button "적용 조건 추가" [ref=f11e181]
- group "예외" [ref=f11e182]:
- generic [ref=f11e184]:
- generic [ref=f11e185]:
- generic [ref=f11e186]: 예외 1
- textbox "예외 1" [ref=f11e187]: 같은 서비스가 브라우저용 public client와 server용 confidential client를 따로 등록할 수 있다. 하나로 합치려고 secret을 브라우저로 내보내지는 않는다.
- generic [ref=f11e188]:
- button "위로" [disabled] [ref=f11e189]
- button "아래로" [ref=f11e190]
- button "삭제" [ref=f11e191]
- generic [ref=f11e192]:
- generic [ref=f11e193]:
- generic [ref=f11e194]: 예외 2
- textbox "예외 2" [ref=f11e195]: backend가 사용자 없이 자기 자격으로 부르는 흐름은 Client Credentials를 쓰는 별도 client다.
- generic [ref=f11e196]:
- button "위로" [ref=f11e197]
- button "아래로" [disabled] [ref=f11e198]
- button "삭제" [ref=f11e199]
- button "예외 추가" [ref=f11e200]
- group "예시" [ref=f11e201]:
- generic [ref=f11e203]:
- generic [ref=f11e204]:
- generic [ref=f11e205]: 예시 1
- textbox "예시 1" [ref=f11e206]: "SPA용 client : public, standard flow만 켜고 implicit flow와 direct grant는 끈다"
- generic [ref=f11e207]:
- button "위로" [disabled] [ref=f11e208]
- button "아래로" [ref=f11e209]
- button "삭제" [ref=f11e210]
- generic [ref=f11e211]:
- generic [ref=f11e212]:
- generic [ref=f11e213]: 예시 2
- textbox "예시 2" [ref=f11e214]: "Mediator용 client : confidential, client_secret_basic으로 token endpoint에서 인증한다"
- generic [ref=f11e215]:
- button "위로" [ref=f11e216]
- button "아래로" [ref=f11e217]
- button "삭제" [ref=f11e218]
- generic [ref=f11e219]:
- generic [ref=f11e220]:
- generic [ref=f11e221]: 예시 3
- textbox "예시 3" [ref=f11e222]: "BFF용 client : confidential, PKCE S256을 함께 쓴다"
- generic [ref=f11e223]:
- button "위로" [ref=f11e224]
- button "아래로" [ref=f11e225]
- button "삭제" [ref=f11e226]
- generic [ref=f11e227]:
- generic [ref=f11e228]:
- generic [ref=f11e229]: 예시 4
- textbox "예시 4" [ref=f11e230]: "Proxy용 client : confidential, oauth2-proxy가 secret과 verifier로 code를 교환한다"
- generic [ref=f11e231]:
- button "위로" [ref=f11e232]
- button "아래로" [ref=f11e233]
- button "삭제" [ref=f11e234]
- generic [ref=f11e235]:
- generic [ref=f11e236]:
- generic [ref=f11e237]: 예시 5
- textbox "예시 5" [ref=f11e238]: confidential client인 Mediator를 써도 access token은 브라우저 응답에 실릴 수 있다
- generic [ref=f11e239]:
- button "위로" [ref=f11e240]
- button "아래로" [disabled] [ref=f11e241]
- button "삭제" [ref=f11e242]
- button "예시 추가" [ref=f11e243]
- generic [ref=f11e244]:
- generic [ref=f11e245]: 마지막 검증일
- textbox "마지막 검증일" [ref=f11e246]
- region [ref=f11e247]:
- generic [ref=f11e248]:
- paragraph [ref=f11e249]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f11e250]
- generic [ref=f11e253]:
- generic [ref=f11e254]:
- navigation "문서 경로" [ref=f11e255]:
- link "Reference" [ref=f11e256] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f11e257]: /
- generic [ref=f11e258]: OAuth/OIDC 인증 경계
- generic [ref=f11e259]: /
- link "KeyCloak Patterns" [ref=f11e260] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Public Client와 Confidential Client 구분 기준" [level=1] [ref=f11e261]
- paragraph [ref=f11e262]: client 종류는 secret을 안전하게 보관할 수 있는지로 정한다. SPA는 보관할 곳이 없어 public client로 등록한다. 종류는 secret이 어디 있는지를 말할 뿐이고, 브라우저에 token이 가는지는 따로 정해진다.
- generic [ref=f11e263]:
- generic [ref=f11e264]:
- term [ref=f11e265]: 유형
- definition [ref=f11e266]: Reference
- generic [ref=f11e267]:
- term [ref=f11e268]: 프로젝트
- definition [ref=f11e269]: KeyCloak Patterns
- generic [ref=f11e270]:
- term [ref=f11e271]: 게시
- definition [ref=f11e272]: 게시 전
- region [ref=f11e273]:
- paragraph [ref=f11e274]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f11e275]
- paragraph [ref=f11e276]: client 종류를 무엇으로 정하는지부터 맞춰야 PKCE와 client 인증을 어디에 둘지 정할 수 있게 된다.기준은 프레임워크나 언어가 아니라 값이 도달하는 범위다. 브라우저에서 실행되는 코드에 넣은 값은 개발자 도구를 열면 그대로 보이기 때문에 SPA는 secret을 가질 수 없고, server와 BFF는 그 값을 process 밖으로 내보내지 않을 수 있어서 secret을 들고 있게 된다.여기서 자주 섞이는 것이 하나 있는데, 종류가 confidential이어도 브라우저에 token이 갈 수 있다. 서로 다른 결정이라서 따로 답해야 한다.
- article [ref=f11e277]:
- region [ref=f11e278]:
- heading "판단 기준" [level=2] [ref=f11e279]
- list [ref=f11e280]:
- listitem [ref=f11e281]:
- generic [ref=f11e282]: "01"
- generic [ref=f11e283]:
- heading "secret을 숨길 수 있는지로 종류를 정한다" [level=3] [ref=f11e284]
- paragraph [ref=f11e285]: 배포물이나 실행 중 memory에서 사용자가 값을 꺼낼 수 있으면 public client가 되고, server 안에만 두고 응답으로 나가지 않게 할 수 있으면 confidential client다. native app은 브라우저가 아니지만 배포물을 뜯으면 값이 나오기 때문에 여기서도 public client로 다루게 된다. 실행 환경의 이름이 아니라 값이 어디까지 가는지로 정한다.
- listitem [ref=f11e286]:
- generic [ref=f11e287]: "02"
- generic [ref=f11e288]:
- heading "public client에서도 Authorization Code Flow에 PKCE를 함께 쓴다" [level=3] [ref=f11e289]
- paragraph [ref=f11e290]: PKCE는 client secret을 대체하는 client 인증 방식이 아니다. authorization request에서 만든 verifier와 token request의 verifier를 연결해 탈취된 authorization code의 교환을 어렵게 만든다. 여기서 S256을 쓴다. plain은 challenge가 verifier 그대로라서 중간에서 본 사람이 그대로 쓸 수 있다.
- listitem [ref=f11e291]:
- generic [ref=f11e292]: "03"
- generic [ref=f11e293]:
- heading "confidential client에도 PKCE를 함께 쓸 수 있다" [level=3] [ref=f11e294]
- paragraph [ref=f11e295]: client 인증이 있어도 PKCE는 여전히 쓸모가 있다. 두 장치가 막는 구간이 서로 달라서 함께 두면 그만큼 좁아지게 된다. 다만 「Authorization Code를 쓴다」와 「PKCE S256까지 설정으로 고정했다」는 서로 다른 주장이다. 설정과 테스트에서 확인한 범위까지만 말할 수 있다.
- listitem [ref=f11e296]:
- generic [ref=f11e297]: "04"
- generic [ref=f11e298]:
- heading "public client에서는 implicit flow와 direct access grant를 끈다" [level=3] [ref=f11e299]
- paragraph [ref=f11e300]: implicit flow는 token을 redirect fragment로 받게 되어서 주소창과 히스토리에 token이 남고, direct access grant는 애플리케이션이 사용자의 아이디와 비밀번호를 직접 받게 되어서 IdP만 알면 되는 값을 애플리케이션이 만지게 된다. 현재 예제에서는 Authorization Code Flow를 사용하므로 implicit flow와 direct access grant를 비활성화했다.
- listitem [ref=f11e301]:
- generic [ref=f11e302]: "05"
- generic [ref=f11e303]:
- heading "종류가 곧 브라우저 token 유무는 아니다" [level=3] [ref=f11e304]
- paragraph [ref=f11e305]: confidential client가 code를 교환해도 그 결과인 access token을 응답 본문으로 브라우저에 건넬 수 있고, 실제로 그렇게 도는 구조가 있다. 종류는 secret을 어디에 두는지를 말하고, token 노출은 어느 계층이 API를 부르는지에 따라 갈린다.
- region [ref=f11e306]:
- heading "적용할 때" [level=2] [ref=f11e307]
- list [ref=f11e308]:
- listitem [ref=f11e309]: 새 OAuth client를 등록할 때
- listitem [ref=f11e310]: SPA와 server 중 어디가 code를 교환할지 정할 때
- listitem [ref=f11e311]: PKCE와 client 인증을 어디에 둘지 정할 때
- listitem [ref=f11e312]: 기존 client의 종류가 맞는지 다시 볼 때
- region [ref=f11e313]:
- heading "예외와 주의" [level=2] [ref=f11e314]
- list [ref=f11e315]:
- listitem [ref=f11e316]: 같은 서비스가 브라우저용 public client와 server용 confidential client를 따로 등록할 수 있다. 하나로 합치려고 secret을 브라우저로 내보내지는 않는다.
- listitem [ref=f11e317]: backend가 사용자 없이 자기 자격으로 부르는 흐름은 Client Credentials를 쓰는 별도 client다.
- region [ref=f11e318]:
- heading "예시" [level=2] [ref=f11e319]
- list [ref=f11e320]:
- listitem [ref=f11e321]: "SPA용 client : public, standard flow만 켜고 implicit flow와 direct grant는 끈다"
- listitem [ref=f11e322]: "Mediator용 client : confidential, client_secret_basic으로 token endpoint에서 인증한다"
- listitem [ref=f11e323]: "BFF용 client : confidential, PKCE S256을 함께 쓴다"
- listitem [ref=f11e324]: "Proxy용 client : confidential, oauth2-proxy가 secret과 verifier로 code를 교환한다"
- listitem [ref=f11e325]: confidential client인 Mediator를 써도 access token은 브라우저 응답에 실릴 수 있다
- paragraph [ref=f11e326]: 마지막 검증
- region [ref=f11e327]:
- paragraph [ref=f11e328]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f11e329]
- list [ref=f11e330]:
- listitem [ref=f11e331]:
- link "SPA를 public client로 등록한 이유를 실제 구성에서 확인할 수 있다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f11e332] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- generic [ref=f11e333]: SPA를 public client로 등록한 이유를 실제 구성에서 확인할 수 있다.
- strong [ref=f11e334]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
- generic [ref=f11e335]:
- listitem [ref=f11e336]:
- link "confidential client를 사용해도 access token 전달 방식은 별도로 설계된다는 예다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f11e337] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f11e338]: confidential client를 사용해도 access token 전달 방식은 별도로 설계된다는 예다.
- strong [ref=f11e339]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f11e340]:
- listitem [ref=f11e341]:
- link "client 종류에 따라 token endpoint의 client 인증 방식이 달라진다. Authorization Code Flow의 Endpoint와 Credential 이동 기준" [ref=f11e342] [cursor=pointer]:
- /url: /references/authorization-code-endpoint-credential-movement
- generic [ref=f11e343]: client 종류에 따라 token endpoint의 client 인증 방식이 달라진다.
- strong [ref=f11e344]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
- generic [ref=f11e345]:
- complementary [ref=f11e346]:
- heading "작업 상태" [level=2] [ref=f11e347]
- status "편집 상태" [ref=f11e348]: 저장됨
- generic [ref=f11e349]:
- generic [ref=f11e350]:
- term [ref=f11e351]: 저장 버전
- definition [ref=f11e352]: "13"
- generic [ref=f11e353]:
- term [ref=f11e354]: 종류
- definition [ref=f11e355]: REFERENCE
- paragraph [ref=f11e356]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f11e357]:
- button "저장" [disabled] [ref=f11e358]
- button "게시" [ref=f11e359]
- paragraph [ref=f11e360]: 버전 13으로 저장했습니다.
@@ -0,0 +1,398 @@
- generic [ref=f12e3]:
- link "본문으로 건너뛰기" [ref=f12e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f12e5]:
- generic [ref=f12e6]:
- link "TechLog Studio" [ref=f12e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f12e8]: Studio
- navigation "Studio 주 탐색" [ref=f12e10]:
- link "작업본" [ref=f12e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f12e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f12e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f12e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f12e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f12e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f12e17]
- main [ref=f12e18]:
- generic [ref=f12e19]:
- generic [ref=f12e20]:
- region [ref=f12e21]:
- generic [ref=f12e22]:
- paragraph [ref=f12e23]: REFERENCE · VERSION 10
- heading "문서 편집" [level=1] [ref=f12e24]
- paragraph [ref=f12e25]: 외부 IdP Federation과 Application 인증 경계
- region [ref=f12e26]:
- generic [ref=f12e27]:
- paragraph [ref=f12e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f12e29]
- generic [ref=f12e30]:
- generic [ref=f12e31]:
- generic [ref=f12e32]: 제목
- textbox "제목" [ref=f12e33]: 외부 IdP Federation과 Application 인증 경계
- generic [ref=f12e34]:
- generic [ref=f12e35]: slug
- textbox "slug" [ref=f12e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: external-idp-federation-application-boundary
- generic [ref=f12e37]:
- generic [ref=f12e38]: 요약
- textbox "요약" [ref=f12e39]: Google 로그인은 다섯 번째 인증 구조가 아니다. Google에서 브로커의 identity brokering과 local session, authorization code를 지나면 애플리케이션이 고르는 것은 여전히 앞의 네 경계 중 하나다.
- generic [ref=f12e40]:
- generic [ref=f12e41]: Topic
- combobox "Topic" [ref=f12e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f12e43]:
- generic [ref=f12e44]: Project
- combobox "Project" [ref=f12e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f12e46]:
- generic [ref=f12e48]:
- generic [ref=f12e49]:
- generic [ref=f12e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f12e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [disabled]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" [selected]
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f12e52]:
- generic [ref=f12e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f12e54]: 이 기준을 프로젝트 결정으로 굳힌 기록이다.
- generic [ref=f12e55]:
- button "위로" [disabled] [ref=f12e56]
- button "아래로" [ref=f12e57]
- button "삭제" [ref=f12e58]
- generic [ref=f12e59]:
- generic [ref=f12e60]:
- generic [ref=f12e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f12e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [disabled]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" [disabled]
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f12e63]:
- generic [ref=f12e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f12e65]: 브로커가 만든 authorization code를 애플리케이션이 받는 흐름이다.
- generic [ref=f12e66]:
- button "위로" [ref=f12e67]
- button "아래로" [ref=f12e68]
- button "삭제" [ref=f12e69]
- generic [ref=f12e70]:
- generic [ref=f12e71]:
- generic [ref=f12e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f12e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [selected]
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" [disabled]
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f12e74]:
- generic [ref=f12e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f12e76]: 외부 IdP가 있어도 애플리케이션 쪽 endpoint 이동은 그대로다.
- generic [ref=f12e77]:
- button "위로" [ref=f12e78]
- button "아래로" [disabled] [ref=f12e79]
- button "삭제" [ref=f12e80]
- button "관계 추가" [ref=f12e81]
- region [ref=f12e82]:
- generic [ref=f12e83]:
- paragraph [ref=f12e84]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f12e85]
- generic [ref=f12e86]:
- generic [ref=f12e87]: 목적
- textbox "목적" [ref=f12e88]: 외부 IdP를 붙이면서 그것을 애플리케이션 인증 구조로 세게 되면 upstream IdP 경계와 애플리케이션 OAuth 경계를 같은 기준으로 묶게 된다. Google은 브로커 앞의 upstream identity provider다. 사용자가 브로커 로그인 화면에서 Google을 고르면 브라우저가 upstream authorization을 하게 되고, 브로커가 그 응답을 검증해 local identity와 연결한 뒤 다시 자기가 만든 authorization code를 애플리케이션으로 보내게 된다. 외부 IdP를 추가해도 애플리케이션 쪽에서 브라우저가 token을 받는지, 어느 계층이 API를 호출하는지는 기존 패턴 선택에 따라 결정한다.
- group "규칙" [ref=f12e89]:
- generic [ref=f12e91]:
- generic [ref=f12e92]:
- generic [ref=f12e93]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f12e94]: 외부 IdP는 브로커 앞단이고 애플리케이션 경계는 그 뒤다
- generic [ref=f12e95]:
- generic [ref=f12e96]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f12e97]: 외부 IdP는 브로커 앞의 provider다. 애플리케이션이 고르는 것은 브로커 뒤의 경계이고, 구조 수를 셀 때 외부 IdP를 목록에 넣으면 성격이 다른 것이 섞인다. upstream IdP의 identity assertion은 Keycloak이 검증한다. 애플리케이션은 Keycloak이 발급한 authorization code와 token을 사용하고 Resource Server도 Keycloak issuer를 검증하므로 애플리케이션의 OAuth 처리 방식은 기존 패턴을 그대로 따른다. UI에서 provider를 고르게 하거나 provider별 계정 연결을 다루는 것은 자연스럽다. 다만 Resource Server의 token 검증이나 애플리케이션 인가가 upstream IdP별로 갈리기 시작하면 브로커 경계가 애플리케이션까지 새고 있는지 본다. 외부 IdP의 token을 애플리케이션이 직접 받아 검증하는 경로를 만들면 브로커가 하던 계정 연결과 정책 판단이 함께 빠진다.
- generic [ref=f12e98]:
- button "위로" [disabled] [ref=f12e99]
- button "아래로" [ref=f12e100]
- button "삭제" [ref=f12e101]
- generic [ref=f12e102]:
- generic [ref=f12e103]:
- generic [ref=f12e104]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f12e105]: stable identity key는 provider와 upstream subject의 조합이다
- generic [ref=f12e106]:
- generic [ref=f12e107]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f12e108]: email은 바뀔 수 있고 다른 계정과 겹칠 수도 있어서 계정을 잇는 열쇠로 맞지 않는다. 어느 provider의 어느 subject인지를 열쇠로 쓴다. email을 열쇠로 쓰면 사용자가 주소를 바꾼 순간 다른 사람이 된다.
- generic [ref=f12e109]:
- button "위로" [ref=f12e110]
- button "아래로" [ref=f12e111]
- button "삭제" [ref=f12e112]
- generic [ref=f12e113]:
- generic [ref=f12e114]:
- generic [ref=f12e115]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f12e116]: email 충돌은 별도의 계정 연결 문제로 다룬다
- generic [ref=f12e117]:
- generic [ref=f12e118]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f12e119]: upstream email이 기존 계정과 같다는 이유로 자동 병합하지 않는다. 같은 주소를 쓰는 다른 사람일 수도 있고 주소를 선점한 공격일 수도 있어서, 기존 계정의 소유권을 증명하는 절차를 따로 둔다.
- generic [ref=f12e120]:
- button "위로" [ref=f12e121]
- button "아래로" [ref=f12e122]
- button "삭제" [ref=f12e123]
- generic [ref=f12e124]:
- generic [ref=f12e125]:
- generic [ref=f12e126]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f12e127]: mock provider로 확인한 범위와 실제 IdP를 구분한다
- generic [ref=f12e128]:
- generic [ref=f12e129]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f12e130]: 브로커와 claim mapping 계약까지만 확인했다. 실제 계정과 공개 HTTPS callback, consent 화면, 도메인 정책은 아직 통과해 보지 않았다. 두 범위를 같은 증거로 쓰면 운영에서 처음 보는 실패를 만난다.
- generic [ref=f12e131]:
- button "위로" [ref=f12e132]
- button "아래로" [disabled] [ref=f12e133]
- button "삭제" [ref=f12e134]
- button "규칙 추가" [ref=f12e135]
- group "적용 조건" [ref=f12e136]:
- generic [ref=f12e138]:
- generic [ref=f12e139]:
- generic [ref=f12e140]: 적용 조건 1
- textbox "적용 조건 1" [ref=f12e141]: 외부 IdP를 붙이며 구조 수를 세려 할 때
- generic [ref=f12e142]:
- button "위로" [disabled] [ref=f12e143]
- button "아래로" [ref=f12e144]
- button "삭제" [ref=f12e145]
- generic [ref=f12e146]:
- generic [ref=f12e147]:
- generic [ref=f12e148]: 적용 조건 2
- textbox "적용 조건 2" [ref=f12e149]: 계정 연결 규칙을 정할 때
- generic [ref=f12e150]:
- button "위로" [ref=f12e151]
- button "아래로" [ref=f12e152]
- button "삭제" [ref=f12e153]
- generic [ref=f12e154]:
- generic [ref=f12e155]:
- generic [ref=f12e156]: 적용 조건 3
- textbox "적용 조건 3" [ref=f12e157]: 검증 범위를 문서로 적을 때
- generic [ref=f12e158]:
- button "위로" [ref=f12e159]
- button "아래로" [ref=f12e160]
- button "삭제" [ref=f12e161]
- generic [ref=f12e162]:
- generic [ref=f12e163]:
- generic [ref=f12e164]: 적용 조건 4
- textbox "적용 조건 4" [ref=f12e165]: 브로커를 거치는 흐름과 직접 OIDC 흐름을 비교할 때
- generic [ref=f12e166]:
- button "위로" [ref=f12e167]
- button "아래로" [disabled] [ref=f12e168]
- button "삭제" [ref=f12e169]
- button "적용 조건 추가" [ref=f12e170]
- group "예외" [ref=f12e171]:
- generic [ref=f12e173]:
- generic [ref=f12e174]:
- generic [ref=f12e175]: 예외 1
- textbox "예외 1" [ref=f12e176]: 애플리케이션이 브로커를 거치지 않고 외부 IdP와 직접 OIDC를 하는 구조라면 그 IdP가 애플리케이션의 issuer가 된다. 그때는 client 종류와 endpoint 기준을 그대로 적용한다.
- generic [ref=f12e177]:
- button "위로" [disabled] [ref=f12e178]
- button "아래로" [ref=f12e179]
- button "삭제" [ref=f12e180]
- generic [ref=f12e181]:
- generic [ref=f12e182]:
- generic [ref=f12e183]: 예외 2
- textbox "예외 2" [ref=f12e184]: 조직 계정만 쓰고 외부 IdP가 하나뿐이면 브로커를 두지 않는 선택도 있다. 그때는 계정 연결 규칙이 필요하지 않다.
- generic [ref=f12e185]:
- button "위로" [ref=f12e186]
- button "아래로" [disabled] [ref=f12e187]
- button "삭제" [ref=f12e188]
- button "예외 추가" [ref=f12e189]
- group "예시" [ref=f12e190]:
- generic [ref=f12e192]:
- generic [ref=f12e193]:
- generic [ref=f12e194]: 예시 1
- textbox "예시 1" [ref=f12e195]: Google 로그인을 추가해도 애플리케이션이 고르는 것은 여전히 네 경계 중 하나다
- generic [ref=f12e196]:
- button "위로" [disabled] [ref=f12e197]
- button "아래로" [ref=f12e198]
- button "삭제" [ref=f12e199]
- generic [ref=f12e200]:
- generic [ref=f12e201]:
- generic [ref=f12e202]: 예시 2
- textbox "예시 2" [ref=f12e203]: 브로커가 provider alias와 upstream subject로 account identity를 정한다
- generic [ref=f12e204]:
- button "위로" [ref=f12e205]
- button "아래로" [ref=f12e206]
- button "삭제" [ref=f12e207]
- generic [ref=f12e208]:
- generic [ref=f12e209]:
- generic [ref=f12e210]: 예시 3
- textbox "예시 3" [ref=f12e211]: 애플리케이션이 신뢰하는 issuer는 외부 IdP가 아니라 브로커다
- generic [ref=f12e212]:
- button "위로" [ref=f12e213]
- button "아래로" [ref=f12e214]
- button "삭제" [ref=f12e215]
- generic [ref=f12e216]:
- generic [ref=f12e217]:
- generic [ref=f12e218]: 예시 4
- textbox "예시 4" [ref=f12e219]: mock OIDC provider로 확인한 것은 브로커와 claim mapping 계약까지다
- generic [ref=f12e220]:
- button "위로" [ref=f12e221]
- button "아래로" [disabled] [ref=f12e222]
- button "삭제" [ref=f12e223]
- button "예시 추가" [ref=f12e224]
- generic [ref=f12e225]:
- generic [ref=f12e226]: 마지막 검증일
- textbox "마지막 검증일" [ref=f12e227]
- region [ref=f12e228]:
- generic [ref=f12e229]:
- paragraph [ref=f12e230]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f12e231]
- generic [ref=f12e234]:
- generic [ref=f12e235]:
- navigation "문서 경로" [ref=f12e236]:
- link "Reference" [ref=f12e237] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f12e238]: /
- generic [ref=f12e239]: OAuth/OIDC 인증 경계
- generic [ref=f12e240]: /
- link "KeyCloak Patterns" [ref=f12e241] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "외부 IdP Federation과 Application 인증 경계" [level=1] [ref=f12e242]
- paragraph [ref=f12e243]: Google 로그인은 다섯 번째 인증 구조가 아니다. Google에서 브로커의 identity brokering과 local session, authorization code를 지나면 애플리케이션이 고르는 것은 여전히 앞의 네 경계 중 하나다.
- generic [ref=f12e244]:
- generic [ref=f12e245]:
- term [ref=f12e246]: 유형
- definition [ref=f12e247]: Reference
- generic [ref=f12e248]:
- term [ref=f12e249]: 프로젝트
- definition [ref=f12e250]: KeyCloak Patterns
- generic [ref=f12e251]:
- term [ref=f12e252]: 게시
- definition [ref=f12e253]: 게시 전
- region [ref=f12e254]:
- paragraph [ref=f12e255]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f12e256]
- paragraph [ref=f12e257]: 외부 IdP를 붙이면서 그것을 애플리케이션 인증 구조로 세게 되면 upstream IdP 경계와 애플리케이션 OAuth 경계를 같은 기준으로 묶게 된다.Google은 브로커 앞의 upstream identity provider다. 사용자가 브로커 로그인 화면에서 Google을 고르면 브라우저가 upstream authorization을 하게 되고, 브로커가 그 응답을 검증해 local identity와 연결한 뒤 다시 자기가 만든 authorization code를 애플리케이션으로 보내게 된다.외부 IdP를 추가해도 애플리케이션 쪽에서 브라우저가 token을 받는지, 어느 계층이 API를 호출하는지는 기존 패턴 선택에 따라 결정한다.
- article [ref=f12e258]:
- region [ref=f12e259]:
- heading "판단 기준" [level=2] [ref=f12e260]
- list [ref=f12e261]:
- listitem [ref=f12e262]:
- generic [ref=f12e263]: "01"
- generic [ref=f12e264]:
- heading "외부 IdP는 브로커 앞단이고 애플리케이션 경계는 그 뒤다" [level=3] [ref=f12e265]
- paragraph [ref=f12e266]: 외부 IdP는 브로커 앞의 provider다. 애플리케이션이 고르는 것은 브로커 뒤의 경계이고, 구조 수를 셀 때 외부 IdP를 목록에 넣으면 성격이 다른 것이 섞인다. upstream IdP의 identity assertion은 Keycloak이 검증한다. 애플리케이션은 Keycloak이 발급한 authorization code와 token을 사용하고 Resource Server도 Keycloak issuer를 검증하므로 애플리케이션의 OAuth 처리 방식은 기존 패턴을 그대로 따른다. UI에서 provider를 고르게 하거나 provider별 계정 연결을 다루는 것은 자연스럽다. 다만 Resource Server의 token 검증이나 애플리케이션 인가가 upstream IdP별로 갈리기 시작하면 브로커 경계가 애플리케이션까지 새고 있는지 본다. 외부 IdP의 token을 애플리케이션이 직접 받아 검증하는 경로를 만들면 브로커가 하던 계정 연결과 정책 판단이 함께 빠진다.
- listitem [ref=f12e267]:
- generic [ref=f12e268]: "02"
- generic [ref=f12e269]:
- heading "stable identity key는 provider와 upstream subject의 조합이다" [level=3] [ref=f12e270]
- paragraph [ref=f12e271]: email은 바뀔 수 있고 다른 계정과 겹칠 수도 있어서 계정을 잇는 열쇠로 맞지 않는다. 어느 provider의 어느 subject인지를 열쇠로 쓴다. email을 열쇠로 쓰면 사용자가 주소를 바꾼 순간 다른 사람이 된다.
- listitem [ref=f12e272]:
- generic [ref=f12e273]: "03"
- generic [ref=f12e274]:
- heading "email 충돌은 별도의 계정 연결 문제로 다룬다" [level=3] [ref=f12e275]
- paragraph [ref=f12e276]: upstream email이 기존 계정과 같다는 이유로 자동 병합하지 않는다. 같은 주소를 쓰는 다른 사람일 수도 있고 주소를 선점한 공격일 수도 있어서, 기존 계정의 소유권을 증명하는 절차를 따로 둔다.
- listitem [ref=f12e277]:
- generic [ref=f12e278]: "04"
- generic [ref=f12e279]:
- heading "mock provider로 확인한 범위와 실제 IdP를 구분한다" [level=3] [ref=f12e280]
- paragraph [ref=f12e281]: 브로커와 claim mapping 계약까지만 확인했다. 실제 계정과 공개 HTTPS callback, consent 화면, 도메인 정책은 아직 통과해 보지 않았다. 두 범위를 같은 증거로 쓰면 운영에서 처음 보는 실패를 만난다.
- region [ref=f12e282]:
- heading "적용할 때" [level=2] [ref=f12e283]
- list [ref=f12e284]:
- listitem [ref=f12e285]: 외부 IdP를 붙이며 구조 수를 세려 할 때
- listitem [ref=f12e286]: 계정 연결 규칙을 정할 때
- listitem [ref=f12e287]: 검증 범위를 문서로 적을 때
- listitem [ref=f12e288]: 브로커를 거치는 흐름과 직접 OIDC 흐름을 비교할 때
- region [ref=f12e289]:
- heading "예외와 주의" [level=2] [ref=f12e290]
- list [ref=f12e291]:
- listitem [ref=f12e292]: 애플리케이션이 브로커를 거치지 않고 외부 IdP와 직접 OIDC를 하는 구조라면 그 IdP가 애플리케이션의 issuer가 된다. 그때는 client 종류와 endpoint 기준을 그대로 적용한다.
- listitem [ref=f12e293]: 조직 계정만 쓰고 외부 IdP가 하나뿐이면 브로커를 두지 않는 선택도 있다. 그때는 계정 연결 규칙이 필요하지 않다.
- region [ref=f12e294]:
- heading "예시" [level=2] [ref=f12e295]
- list [ref=f12e296]:
- listitem [ref=f12e297]: Google 로그인을 추가해도 애플리케이션이 고르는 것은 여전히 네 경계 중 하나다
- listitem [ref=f12e298]: 브로커가 provider alias와 upstream subject로 account identity를 정한다
- listitem [ref=f12e299]: 애플리케이션이 신뢰하는 issuer는 외부 IdP가 아니라 브로커다
- listitem [ref=f12e300]: mock OIDC provider로 확인한 것은 브로커와 claim mapping 계약까지다
- paragraph [ref=f12e301]: 마지막 검증
- region [ref=f12e302]:
- paragraph [ref=f12e303]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f12e304]
- list [ref=f12e305]:
- listitem [ref=f12e306]:
- link "브로커가 만든 authorization code를 애플리케이션이 받는 흐름이다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f12e307] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- generic [ref=f12e308]: 브로커가 만든 authorization code를 애플리케이션이 받는 흐름이다.
- strong [ref=f12e309]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
- generic [ref=f12e310]:
- listitem [ref=f12e311]:
- link "외부 IdP가 있어도 애플리케이션 쪽 endpoint 이동은 그대로다. Authorization Code Flow의 Endpoint와 Credential 이동 기준" [ref=f12e312] [cursor=pointer]:
- /url: /references/authorization-code-endpoint-credential-movement
- generic [ref=f12e313]: 외부 IdP가 있어도 애플리케이션 쪽 endpoint 이동은 그대로다.
- strong [ref=f12e314]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
- generic [ref=f12e315]:
- complementary [ref=f12e316]:
- heading "작업 상태" [level=2] [ref=f12e317]
- status "편집 상태" [ref=f12e318]: 저장됨
- generic [ref=f12e319]:
- generic [ref=f12e320]:
- term [ref=f12e321]: 저장 버전
- definition [ref=f12e322]: "10"
- generic [ref=f12e323]:
- term [ref=f12e324]: 종류
- definition [ref=f12e325]: REFERENCE
- paragraph [ref=f12e326]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f12e327]:
- button "저장" [disabled] [ref=f12e328]
- button "게시" [ref=f12e329]
- paragraph [ref=f12e330]: 버전 10으로 저장했습니다.
@@ -0,0 +1,474 @@
- generic [ref=f13e3]:
- link "본문으로 건너뛰기" [ref=f13e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f13e5]:
- generic [ref=f13e6]:
- link "TechLog Studio" [ref=f13e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f13e8]: Studio
- navigation "Studio 주 탐색" [ref=f13e10]:
- link "작업본" [ref=f13e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f13e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f13e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f13e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f13e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f13e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f13e17]
- main [ref=f13e18]:
- generic [ref=f13e19]:
- generic [ref=f13e20]:
- region [ref=f13e21]:
- generic [ref=f13e22]:
- paragraph [ref=f13e23]: REFERENCE · VERSION 11
- heading "문서 편집" [level=1] [ref=f13e24]
- paragraph [ref=f13e25]: Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
- region [ref=f13e26]:
- generic [ref=f13e27]:
- paragraph [ref=f13e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f13e29]
- generic [ref=f13e30]:
- generic [ref=f13e31]:
- generic [ref=f13e32]: 제목
- textbox "제목" [ref=f13e33]: Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
- generic [ref=f13e34]:
- generic [ref=f13e35]: slug
- textbox "slug" [ref=f13e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: forward-auth-identity-header-trust
- generic [ref=f13e37]:
- generic [ref=f13e38]: 요약
- textbox "요약" [ref=f13e39]: upstream이 사용자를 판단하는 근거가 헤더 하나뿐인 구조에서, 그 헤더를 믿을 수 있게 만드는 조건을 모았다. 외부 경로 차단, 동명 헤더 덮어쓰기, internal credential 검증이 서로 다른 곳에 함께 있어야 한다.
- generic [ref=f13e40]:
- generic [ref=f13e41]: Topic
- combobox "Topic" [ref=f13e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f13e43]:
- generic [ref=f13e44]: Project
- combobox "Project" [ref=f13e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f13e46]:
- generic [ref=f13e48]:
- generic [ref=f13e49]:
- generic [ref=f13e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f13e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" [disabled]
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f13e52]:
- generic [ref=f13e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f13e54]: 이 기준의 다섯 조건을 실제 설정에서 확인한 기록이다.
- generic [ref=f13e55]:
- button "위로" [disabled] [ref=f13e56]
- button "아래로" [ref=f13e57]
- button "삭제" [ref=f13e58]
- generic [ref=f13e59]:
- generic [ref=f13e60]:
- generic [ref=f13e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f13e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" [selected]
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f13e63]:
- generic [ref=f13e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f13e65]: 헤더를 어디까지 늘릴지가 이 기준의 미결 항목이다.
- generic [ref=f13e66]:
- button "위로" [ref=f13e67]
- button "아래로" [ref=f13e68]
- button "삭제" [ref=f13e69]
- generic [ref=f13e70]:
- generic [ref=f13e71]:
- generic [ref=f13e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f13e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" [disabled]
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [selected]
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f13e74]:
- generic [ref=f13e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f13e76]: identity 헤더를 JWT나 session과 같은 이름으로 부르지 않는다.
- generic [ref=f13e77]:
- button "위로" [ref=f13e78]
- button "아래로" [disabled] [ref=f13e79]
- button "삭제" [ref=f13e80]
- button "관계 추가" [ref=f13e81]
- region [ref=f13e82]:
- generic [ref=f13e83]:
- paragraph [ref=f13e84]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f13e85]
- generic [ref=f13e86]:
- generic [ref=f13e87]: 목적
- textbox "목적" [ref=f13e88]: 외부 요청이 edge를 지나 인증되고 upstream으로 가는 구조에서, upstream이 사용자를 판단하는 근거는 헤더 하나다. 같은 이름의 헤더를 인증을 마친 edge가 만들 수도 있고 공격자가 요청에 직접 적어 보낼 수도 있다. upstream이 받는 요청에서 이 둘은 구분되지 않는다. identity header를 upstream에서 사용하려면 먼저 그 헤더가 edge를 통해 생성됐음을 보장하는 경로와 검증 방법을 정한다.
- group "규칙" [ref=f13e89]:
- generic [ref=f13e91]:
- generic [ref=f13e92]:
- generic [ref=f13e93]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f13e94]: 외부에서 upstream과 auth proxy에 직접 닿지 못하게 한다
- generic [ref=f13e95]:
- generic [ref=f13e96]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f13e97]: edge만 공개하고 나머지는 내부 network에 두면서 host port로 노출하지 않는다. 이걸 안 하면 공격자가 edge를 건너뛰고 upstream을 직접 부른다. 그때는 헤더를 아무리 검사해도 공격자가 그 헤더를 마음대로 쓸 수 있어서 의미가 없다.
- generic [ref=f13e98]:
- button "위로" [disabled] [ref=f13e99]
- button "아래로" [ref=f13e100]
- button "삭제" [ref=f13e101]
- generic [ref=f13e102]:
- generic [ref=f13e103]:
- generic [ref=f13e104]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f13e105]: client가 보낸 동명 헤더를 항상 덮어쓴다
- generic [ref=f13e106]:
- generic [ref=f13e107]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f13e108]: merge가 아니라 덮어쓰기로 채우고, 인증 결과에서 복사한 값만 upstream으로 보낸다. merge로 두면 client가 보낸 값이 앞이나 뒤에 함께 붙고, 어느 쪽을 읽을지는 upstream 구현에 달려 있다. trusted proxy 범위도 같이 좁힌다. 넓게 잡으면 같은 network 안의 다른 workload가 edge인 척할 수 있고, forwarded 계열 헤더를 믿는 설정에서는 그 범위가 곧 신뢰 경계다.
- generic [ref=f13e109]:
- button "위로" [ref=f13e110]
- button "아래로" [ref=f13e111]
- button "삭제" [ref=f13e112]
- generic [ref=f13e113]:
- generic [ref=f13e114]:
- generic [ref=f13e115]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f13e116]: auth endpoint는 subrequest 전용으로 둔다
- generic [ref=f13e117]:
- generic [ref=f13e118]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f13e119]: "이 endpoint는 외부 client가 쓰라고 만든 것이 아니다. proxy가 만드는 subrequest만 들어가게 하고 외부 호출에는 응답하지 않게 둔다. Nginx라면 `internal` location이 그 역할을 한다."
- generic [ref=f13e120]:
- button "위로" [ref=f13e121]
- button "아래로" [ref=f13e122]
- button "삭제" [ref=f13e123]
- generic [ref=f13e124]:
- generic [ref=f13e125]:
- generic [ref=f13e126]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f13e127]: upstream이 헤더 존재만 보지 않는다
- generic [ref=f13e128]:
- generic [ref=f13e129]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f13e130]: 배포 시 주입한 internal credential과 요청 값을 비교한다. 비교 구현은 입력값의 일치 길이에 따라 실행 시간이 크게 달라지지 않는 방식을 사용한다. internal credential 검증을 controller마다 반복하면 새 endpoint에서 누락될 수 있다. 운영에서는 filter, interceptor, security chain 등 공통 처리 경로에 적용한다.
- generic [ref=f13e131]:
- button "위로" [ref=f13e132]
- button "아래로" [ref=f13e133]
- button "삭제" [ref=f13e134]
- generic [ref=f13e135]:
- generic [ref=f13e136]:
- generic [ref=f13e137]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f13e138]: Network 격리와 헤더 검증을 모두 적용한다
- generic [ref=f13e139]:
- generic [ref=f13e140]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f13e141]: 격리는 밖에서 들어오는 직접 접근을 막고 헤더 검증은 안에서 만들어진 위조를 막는다. 막는 대상이 달라서 하나로 다른 하나를 대체했다고 쓸 수 없다.
- generic [ref=f13e142]:
- button "위로" [ref=f13e143]
- button "아래로" [ref=f13e144]
- button "삭제" [ref=f13e145]
- generic [ref=f13e146]:
- generic [ref=f13e147]:
- generic [ref=f13e148]: 규칙 6 제목
- textbox "규칙 6 제목" [ref=f13e149]: 전달할 헤더를 allowlist로 고정한다
- generic [ref=f13e150]:
- generic [ref=f13e151]: 규칙 6 본문
- textbox "규칙 6 본문" [ref=f13e152]: 복사할 응답 헤더 목록을 정해 두고 그 밖은 버린다. 늘릴 때마다 claim 출처와 다중 값 구분자, escaping, 최대 크기, upstream 검증 계약을 다시 정해야 한다. user와 email만 전달하는 구조는 누가 왔는지만 말하고 무엇을 해도 되는지는 말하지 않는다. role이 바뀌었을 때 proxy session과 downstream 인가가 언제 따라가는지도 따로 정한다.
- generic [ref=f13e153]:
- button "위로" [ref=f13e154]
- button "아래로" [ref=f13e155]
- button "삭제" [ref=f13e156]
- generic [ref=f13e157]:
- generic [ref=f13e158]:
- generic [ref=f13e159]: 규칙 7 제목
- textbox "규칙 7 제목" [ref=f13e160]: 검사 지점은 요청 실패가 아니라 응답의 사용자다
- generic [ref=f13e161]:
- generic [ref=f13e162]: 규칙 7 본문
- textbox "규칙 7 본문" [ref=f13e163]: 위조 헤더를 얹은 정상 session 요청은 정상 session이니 200이 되는 것이 맞다. 확인할 값은 그 응답의 사용자가 위조 값인지 실제 인증된 사용자인지다. 요청이 실패하는지만 보면 덮어쓰기가 동작하는지 알 수 없다.
- generic [ref=f13e164]:
- button "위로" [ref=f13e165]
- button "아래로" [ref=f13e166]
- button "삭제" [ref=f13e167]
- generic [ref=f13e168]:
- generic [ref=f13e169]:
- generic [ref=f13e170]: 규칙 8 제목
- textbox "규칙 8 제목" [ref=f13e171]: 지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다
- generic [ref=f13e172]:
- generic [ref=f13e173]: 규칙 8 본문
- textbox "규칙 8 본문" [ref=f13e174]: 이 기준에서 실제 fixture로 확인한 것은 외부 경로 차단, 헤더 덮어쓰기, auth endpoint 내부 전용 지정, upstream의 internal credential 확인이다. 운영에서는 여기에 더 필요하다. 공유 secret을 secret manager에서 주입하고 교체 절차를 두는 것, network policy로 경로를 강제하는 것, 그리고 더 강하게 묶으려면 mTLS나 workload identity를 쓰는 것이다. 두 묶음을 같은 문단에 섞어 적지 않는다.
- generic [ref=f13e175]:
- button "위로" [ref=f13e176]
- button "아래로" [disabled] [ref=f13e177]
- button "삭제" [ref=f13e178]
- button "규칙 추가" [ref=f13e179]
- group "적용 조건" [ref=f13e180]:
- generic [ref=f13e182]:
- generic [ref=f13e183]:
- generic [ref=f13e184]: 적용 조건 1
- textbox "적용 조건 1" [ref=f13e185]: upstream에 OAuth client나 JWT 검증 코드를 넣기 어려울 때
- generic [ref=f13e186]:
- button "위로" [disabled] [ref=f13e187]
- button "아래로" [ref=f13e188]
- button "삭제" [ref=f13e189]
- generic [ref=f13e190]:
- generic [ref=f13e191]:
- generic [ref=f13e192]: 적용 조건 2
- textbox "적용 조건 2" [ref=f13e193]: 여러 legacy service 앞에 같은 로그인 정책을 둘 때
- generic [ref=f13e194]:
- button "위로" [ref=f13e195]
- button "아래로" [ref=f13e196]
- button "삭제" [ref=f13e197]
- generic [ref=f13e198]:
- generic [ref=f13e199]:
- generic [ref=f13e200]: 적용 조건 3
- textbox "적용 조건 3" [ref=f13e201]: edge에서 정책을 강제할 수 있을 때
- generic [ref=f13e202]:
- button "위로" [ref=f13e203]
- button "아래로" [ref=f13e204]
- button "삭제" [ref=f13e205]
- generic [ref=f13e206]:
- generic [ref=f13e207]:
- generic [ref=f13e208]: 적용 조건 4
- textbox "적용 조건 4" [ref=f13e209]: 이미 forward-auth를 쓰고 있는 구조를 점검할 때
- generic [ref=f13e210]:
- button "위로" [ref=f13e211]
- button "아래로" [disabled] [ref=f13e212]
- button "삭제" [ref=f13e213]
- button "적용 조건 추가" [ref=f13e214]
- group "예외" [ref=f13e215]:
- generic [ref=f13e217]:
- generic [ref=f13e218]:
- generic [ref=f13e219]: 예외 1
- textbox "예외 1" [ref=f13e220]: backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
- generic [ref=f13e221]:
- button "위로" [disabled] [ref=f13e222]
- button "아래로" [ref=f13e223]
- button "삭제" [ref=f13e224]
- generic [ref=f13e225]:
- generic [ref=f13e226]:
- generic [ref=f13e227]: 예외 2
- textbox "예외 2" [ref=f13e228]: 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF 구조가 더 자연스럽다.
- generic [ref=f13e229]:
- button "위로" [ref=f13e230]
- button "아래로" [ref=f13e231]
- button "삭제" [ref=f13e232]
- generic [ref=f13e233]:
- generic [ref=f13e234]:
- generic [ref=f13e235]: 예외 3
- textbox "예외 3" [ref=f13e236]: 임의 경로와 body, streaming을 그대로 넘기는 범용 reverse proxy가 필요하면 URI rewrite와 timeout, 응답 헤더 처리를 따로 설계해야 한다.
- generic [ref=f13e237]:
- button "위로" [ref=f13e238]
- button "아래로" [disabled] [ref=f13e239]
- button "삭제" [ref=f13e240]
- button "예외 추가" [ref=f13e241]
- group "예시" [ref=f13e242]:
- generic [ref=f13e244]:
- generic [ref=f13e245]:
- generic [ref=f13e246]: 예시 1
- textbox "예시 1" [ref=f13e247]: 외부에는 edge만 공개하고 app과 auth proxy의 port는 host에 publish하지 않는다
- generic [ref=f13e248]:
- button "위로" [disabled] [ref=f13e249]
- button "아래로" [ref=f13e250]
- button "삭제" [ref=f13e251]
- generic [ref=f13e252]:
- generic [ref=f13e253]:
- generic [ref=f13e254]: 예시 2
- textbox "예시 2" [ref=f13e255]: 정상 session에 위조 헤더를 얹은 요청은 200을 받지만 응답의 사용자는 실제 사용자다
- generic [ref=f13e256]:
- button "위로" [ref=f13e257]
- button "아래로" [ref=f13e258]
- button "삭제" [ref=f13e259]
- generic [ref=f13e260]:
- generic [ref=f13e261]:
- generic [ref=f13e262]: 예시 3
- textbox "예시 3" [ref=f13e263]: 외부에서 auth endpoint를 직접 부르면 404가 된다
- generic [ref=f13e264]:
- button "위로" [ref=f13e265]
- button "아래로" [ref=f13e266]
- button "삭제" [ref=f13e267]
- generic [ref=f13e268]:
- generic [ref=f13e269]:
- generic [ref=f13e270]: 예시 4
- textbox "예시 4" [ref=f13e271]: upstream은 user 헤더와 internal token을 함께 확인하고 하나라도 어긋나면 401을 돌려준다
- generic [ref=f13e272]:
- button "위로" [ref=f13e273]
- button "아래로" [ref=f13e274]
- button "삭제" [ref=f13e275]
- generic [ref=f13e276]:
- generic [ref=f13e277]:
- generic [ref=f13e278]: 예시 5
- textbox "예시 5" [ref=f13e279]: 내부 검사가 controller 하나에만 있으면 새 endpoint에는 보호가 따라오지 않는다
- generic [ref=f13e280]:
- button "위로" [ref=f13e281]
- button "아래로" [disabled] [ref=f13e282]
- button "삭제" [ref=f13e283]
- button "예시 추가" [ref=f13e284]
- generic [ref=f13e285]:
- generic [ref=f13e286]: 마지막 검증일
- textbox "마지막 검증일" [ref=f13e287]
- region [ref=f13e288]:
- generic [ref=f13e289]:
- paragraph [ref=f13e290]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f13e291]
- generic [ref=f13e294]:
- generic [ref=f13e295]:
- navigation "문서 경로" [ref=f13e296]:
- link "Reference" [ref=f13e297] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f13e298]: /
- generic [ref=f13e299]: OAuth/OIDC 인증 경계
- generic [ref=f13e300]: /
- link "KeyCloak Patterns" [ref=f13e301] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" [level=1] [ref=f13e302]
- paragraph [ref=f13e303]: upstream이 사용자를 판단하는 근거가 헤더 하나뿐인 구조에서, 그 헤더를 믿을 수 있게 만드는 조건을 모았다. 외부 경로 차단, 동명 헤더 덮어쓰기, internal credential 검증이 서로 다른 곳에 함께 있어야 한다.
- generic [ref=f13e304]:
- generic [ref=f13e305]:
- term [ref=f13e306]: 유형
- definition [ref=f13e307]: Reference
- generic [ref=f13e308]:
- term [ref=f13e309]: 프로젝트
- definition [ref=f13e310]: KeyCloak Patterns
- generic [ref=f13e311]:
- term [ref=f13e312]: 게시
- definition [ref=f13e313]: 게시 전
- region [ref=f13e314]:
- paragraph [ref=f13e315]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f13e316]
- paragraph [ref=f13e317]: 외부 요청이 edge를 지나 인증되고 upstream으로 가는 구조에서, upstream이 사용자를 판단하는 근거는 헤더 하나다.같은 이름의 헤더를 인증을 마친 edge가 만들 수도 있고 공격자가 요청에 직접 적어 보낼 수도 있다. upstream이 받는 요청에서 이 둘은 구분되지 않는다.identity header를 upstream에서 사용하려면 먼저 그 헤더가 edge를 통해 생성됐음을 보장하는 경로와 검증 방법을 정한다.
- article [ref=f13e318]:
- region [ref=f13e319]:
- heading "판단 기준" [level=2] [ref=f13e320]
- list [ref=f13e321]:
- listitem [ref=f13e322]:
- generic [ref=f13e323]: "01"
- generic [ref=f13e324]:
- heading "외부에서 upstream과 auth proxy에 직접 닿지 못하게 한다" [level=3] [ref=f13e325]
- paragraph [ref=f13e326]: edge만 공개하고 나머지는 내부 network에 두면서 host port로 노출하지 않는다. 이걸 안 하면 공격자가 edge를 건너뛰고 upstream을 직접 부른다. 그때는 헤더를 아무리 검사해도 공격자가 그 헤더를 마음대로 쓸 수 있어서 의미가 없다.
- listitem [ref=f13e327]:
- generic [ref=f13e328]: "02"
- generic [ref=f13e329]:
- heading "client가 보낸 동명 헤더를 항상 덮어쓴다" [level=3] [ref=f13e330]
- paragraph [ref=f13e331]: merge가 아니라 덮어쓰기로 채우고, 인증 결과에서 복사한 값만 upstream으로 보낸다. merge로 두면 client가 보낸 값이 앞이나 뒤에 함께 붙고, 어느 쪽을 읽을지는 upstream 구현에 달려 있다. trusted proxy 범위도 같이 좁힌다. 넓게 잡으면 같은 network 안의 다른 workload가 edge인 척할 수 있고, forwarded 계열 헤더를 믿는 설정에서는 그 범위가 곧 신뢰 경계다.
- listitem [ref=f13e332]:
- generic [ref=f13e333]: "03"
- generic [ref=f13e334]:
- heading "auth endpoint는 subrequest 전용으로 둔다" [level=3] [ref=f13e335]
- paragraph [ref=f13e336]: "이 endpoint는 외부 client가 쓰라고 만든 것이 아니다. proxy가 만드는 subrequest만 들어가게 하고 외부 호출에는 응답하지 않게 둔다. Nginx라면 `internal` location이 그 역할을 한다."
- listitem [ref=f13e337]:
- generic [ref=f13e338]: "04"
- generic [ref=f13e339]:
- heading "upstream이 헤더 존재만 보지 않는다" [level=3] [ref=f13e340]
- paragraph [ref=f13e341]: 배포 시 주입한 internal credential과 요청 값을 비교한다. 비교 구현은 입력값의 일치 길이에 따라 실행 시간이 크게 달라지지 않는 방식을 사용한다. internal credential 검증을 controller마다 반복하면 새 endpoint에서 누락될 수 있다. 운영에서는 filter, interceptor, security chain 등 공통 처리 경로에 적용한다.
- listitem [ref=f13e342]:
- generic [ref=f13e343]: "05"
- generic [ref=f13e344]:
- heading "Network 격리와 헤더 검증을 모두 적용한다" [level=3] [ref=f13e345]
- paragraph [ref=f13e346]: 격리는 밖에서 들어오는 직접 접근을 막고 헤더 검증은 안에서 만들어진 위조를 막는다. 막는 대상이 달라서 하나로 다른 하나를 대체했다고 쓸 수 없다.
- listitem [ref=f13e347]:
- generic [ref=f13e348]: "06"
- generic [ref=f13e349]:
- heading "전달할 헤더를 allowlist로 고정한다" [level=3] [ref=f13e350]
- paragraph [ref=f13e351]: 복사할 응답 헤더 목록을 정해 두고 그 밖은 버린다. 늘릴 때마다 claim 출처와 다중 값 구분자, escaping, 최대 크기, upstream 검증 계약을 다시 정해야 한다. user와 email만 전달하는 구조는 누가 왔는지만 말하고 무엇을 해도 되는지는 말하지 않는다. role이 바뀌었을 때 proxy session과 downstream 인가가 언제 따라가는지도 따로 정한다.
- listitem [ref=f13e352]:
- generic [ref=f13e353]: "07"
- generic [ref=f13e354]:
- heading "검사 지점은 요청 실패가 아니라 응답의 사용자다" [level=3] [ref=f13e355]
- paragraph [ref=f13e356]: 위조 헤더를 얹은 정상 session 요청은 정상 session이니 200이 되는 것이 맞다. 확인할 값은 그 응답의 사용자가 위조 값인지 실제 인증된 사용자인지다. 요청이 실패하는지만 보면 덮어쓰기가 동작하는지 알 수 없다.
- listitem [ref=f13e357]:
- generic [ref=f13e358]: "08"
- generic [ref=f13e359]:
- heading "지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다" [level=3] [ref=f13e360]
- paragraph [ref=f13e361]: 이 기준에서 실제 fixture로 확인한 것은 외부 경로 차단, 헤더 덮어쓰기, auth endpoint 내부 전용 지정, upstream의 internal credential 확인이다. 운영에서는 여기에 더 필요하다. 공유 secret을 secret manager에서 주입하고 교체 절차를 두는 것, network policy로 경로를 강제하는 것, 그리고 더 강하게 묶으려면 mTLS나 workload identity를 쓰는 것이다. 두 묶음을 같은 문단에 섞어 적지 않는다.
- region [ref=f13e362]:
- heading "적용할 때" [level=2] [ref=f13e363]
- list [ref=f13e364]:
- listitem [ref=f13e365]: upstream에 OAuth client나 JWT 검증 코드를 넣기 어려울 때
- listitem [ref=f13e366]: 여러 legacy service 앞에 같은 로그인 정책을 둘 때
- listitem [ref=f13e367]: edge에서 정책을 강제할 수 있을 때
- listitem [ref=f13e368]: 이미 forward-auth를 쓰고 있는 구조를 점검할 때
- region [ref=f13e369]:
- heading "예외와 주의" [level=2] [ref=f13e370]
- list [ref=f13e371]:
- listitem [ref=f13e372]: backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
- listitem [ref=f13e373]: 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF 구조가 더 자연스럽다.
- listitem [ref=f13e374]: 임의 경로와 body, streaming을 그대로 넘기는 범용 reverse proxy가 필요하면 URI rewrite와 timeout, 응답 헤더 처리를 따로 설계해야 한다.
- region [ref=f13e375]:
- heading "예시" [level=2] [ref=f13e376]
- list [ref=f13e377]:
- listitem [ref=f13e378]: 외부에는 edge만 공개하고 app과 auth proxy의 port는 host에 publish하지 않는다
- listitem [ref=f13e379]: 정상 session에 위조 헤더를 얹은 요청은 200을 받지만 응답의 사용자는 실제 사용자다
- listitem [ref=f13e380]: 외부에서 auth endpoint를 직접 부르면 404가 된다
- listitem [ref=f13e381]: upstream은 user 헤더와 internal token을 함께 확인하고 하나라도 어긋나면 401을 돌려준다
- listitem [ref=f13e382]: 내부 검사가 controller 하나에만 있으면 새 endpoint에는 보호가 따라오지 않는다
- paragraph [ref=f13e383]: 마지막 검증
- region [ref=f13e384]:
- paragraph [ref=f13e385]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f13e386]
- list [ref=f13e387]:
- listitem [ref=f13e388]:
- link "이 기준의 다섯 조건을 실제 설정에서 확인한 기록이다. Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f13e389] [cursor=pointer]:
- /url: /cases/identity-header-trust
- generic [ref=f13e390]: 이 기준의 다섯 조건을 실제 설정에서 확인한 기록이다.
- strong [ref=f13e391]: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
- generic [ref=f13e392]:
- complementary [ref=f13e393]:
- heading "작업 상태" [level=2] [ref=f13e394]
- status "편집 상태" [ref=f13e395]: 저장됨
- generic [ref=f13e396]:
- generic [ref=f13e397]:
- term [ref=f13e398]: 저장 버전
- definition [ref=f13e399]: "11"
- generic [ref=f13e400]:
- term [ref=f13e401]: 종류
- definition [ref=f13e402]: REFERENCE
- paragraph [ref=f13e403]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f13e404]:
- button "저장" [disabled] [ref=f13e405]
- button "게시" [ref=f13e406]
- paragraph [ref=f13e407]: 버전 11으로 저장했습니다.
@@ -0,0 +1,472 @@
- generic [ref=f14e3]:
- link "본문으로 건너뛰기" [ref=f14e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f14e5]:
- generic [ref=f14e6]:
- link "TechLog Studio" [ref=f14e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f14e8]: Studio
- navigation "Studio 주 탐색" [ref=f14e10]:
- link "작업본" [ref=f14e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f14e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f14e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f14e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f14e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f14e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f14e17]
- main [ref=f14e18]:
- generic [ref=f14e19]:
- generic [ref=f14e20]:
- region [ref=f14e21]:
- generic [ref=f14e22]:
- paragraph [ref=f14e23]: REFERENCE · VERSION 11
- heading "문서 편집" [level=1] [ref=f14e24]
- paragraph [ref=f14e25]: BFF 인증 구조 설계 기준
- region [ref=f14e26]:
- generic [ref=f14e27]:
- paragraph [ref=f14e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f14e29]
- generic [ref=f14e30]:
- generic [ref=f14e31]:
- generic [ref=f14e32]: 제목
- textbox "제목" [ref=f14e33]: BFF 인증 구조 설계 기준
- generic [ref=f14e34]:
- generic [ref=f14e35]: slug
- textbox "slug" [ref=f14e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: bff-authentication-design-criteria
- generic [ref=f14e37]:
- generic [ref=f14e38]: 요약
- textbox "요약" [ref=f14e39]: BFF가 OAuth token을 server-side에서 관리하고 브라우저는 session cookie로 BFF를 호출할 때 필요한 설계 항목을 정리한다. CSRF 검증, authorized client 저장소, logout, downstream 오류 처리가 핵심이다.
- generic [ref=f14e40]:
- generic [ref=f14e41]: Topic
- combobox "Topic" [ref=f14e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f14e43]:
- generic [ref=f14e44]: Project
- combobox "Project" [ref=f14e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f14e46]:
- generic [ref=f14e48]:
- generic [ref=f14e49]:
- generic [ref=f14e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f14e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f14e52]:
- generic [ref=f14e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f14e54]: 이 기준의 항목 중 실제로 구현된 것과 비어 있는 것을 센 기록이다.
- generic [ref=f14e55]:
- button "위로" [disabled] [ref=f14e56]
- button "아래로" [ref=f14e57]
- button "삭제" [ref=f14e58]
- generic [ref=f14e59]:
- generic [ref=f14e60]:
- generic [ref=f14e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f14e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [selected]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f14e63]:
- generic [ref=f14e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f14e65]: 저장소 항목이 아직 답이 없는 질문으로 남아 있다.
- generic [ref=f14e66]:
- button "위로" [ref=f14e67]
- button "아래로" [ref=f14e68]
- button "삭제" [ref=f14e69]
- generic [ref=f14e70]:
- generic [ref=f14e71]:
- generic [ref=f14e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f14e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건" [disabled]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f14e74]:
- generic [ref=f14e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f14e76]: 어느 저장소에 둘지가 이 기준의 미결 항목이다.
- generic [ref=f14e77]:
- button "위로" [ref=f14e78]
- button "아래로" [ref=f14e79]
- button "삭제" [ref=f14e80]
- generic [ref=f14e81]:
- generic [ref=f14e82]:
- generic [ref=f14e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f14e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건" [selected]
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f14e85]:
- generic [ref=f14e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f14e87]: 이 결정이 PROPOSED인 동안 실제 적용 기준은 이 문서다.
- generic [ref=f14e88]:
- button "위로" [ref=f14e89]
- button "아래로" [disabled] [ref=f14e90]
- button "삭제" [ref=f14e91]
- button "관계 추가" [ref=f14e92]
- region [ref=f14e93]:
- generic [ref=f14e94]:
- paragraph [ref=f14e95]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f14e96]
- generic [ref=f14e97]:
- generic [ref=f14e98]: 목적
- textbox "목적" [ref=f14e99]: BFF 구조에서는 BFF가 authorization code를 token으로 교환하고 access token을 사용해 Resource Server를 호출한다. 따라서 session과 authorized client를 함께 관리하는 보안 구성요소로 본다. cookie가 credential이 되면 브라우저가 요청마다 자동으로 붙인다. 값을 바꾸는 요청은 사용자의 의도인지 따로 확인해야 한다. 그리고 재시작과 replica 이동을 견딜 저장소도 함께 필요해진다. 여기 있는 것은 「BFF를 쓴다」로 답이 되지 않는 항목들이다.
- group "규칙" [ref=f14e100]:
- generic [ref=f14e102]:
- generic [ref=f14e103]:
- generic [ref=f14e104]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f14e105]: 브라우저에는 OAuth token을 전달하지 않는다
- generic [ref=f14e106]:
- generic [ref=f14e107]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f14e108]: "access token과 refresh token은 server-side authorized client에 보관한다. 브라우저가 token을 직접 사용할 필요가 없도록 BFF가 downstream 요청의 `Authorization` 헤더를 만든다. session cookie는 downstream으로 전달하지 않는다. BFF가 session을 애플리케이션 credential로 소비하고, Resource Server가 아는 Bearer 요청을 새로 만든다. 두 credential은 같은 요청 처리 안에 있지만 검증하는 주체가 다르다."
- generic [ref=f14e109]:
- button "위로" [disabled] [ref=f14e110]
- button "아래로" [ref=f14e111]
- button "삭제" [ref=f14e112]
- generic [ref=f14e113]:
- generic [ref=f14e114]:
- generic [ref=f14e115]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f14e116]: cookie가 credential이면 상태 변경 요청에 CSRF 검증을 둔다
- generic [ref=f14e117]:
- generic [ref=f14e118]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f14e119]: session cookie는 브라우저가 자동으로 전송하므로 상태 변경 endpoint에는 CSRF 검증을 적용한다. 현재 구성은 JavaScript가 CSRF cookie를 읽어 요청 헤더에 같은 값을 전달하는 방식을 사용한다. 노출 값과 제출 값이 다를 수 있다. 응답 본문의 token이 가려진 값이면 헤더에 넣는 값은 cookie에서 읽어야 한다. 두 값을 같다고 가정하고 구현하면 클라이언트가 그대로 403을 받는다. SameSite와 CSRF token은 역할이 다르다. SameSite는 특정 cross-site 요청에서 cookie 전송을 제한하는 브라우저 정책이고, CSRF token은 cookie가 포함된 상태 변경 요청을 서버가 추가로 검증하는 값이다. 같은 site로 계산되는 다른 origin 요청도 고려해야 한다.
- generic [ref=f14e120]:
- button "위로" [ref=f14e121]
- button "아래로" [ref=f14e122]
- button "삭제" [ref=f14e123]
- generic [ref=f14e124]:
- generic [ref=f14e125]:
- generic [ref=f14e126]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f14e127]: session과 authorized client의 수명주기를 따로 설계한다
- generic [ref=f14e128]:
- generic [ref=f14e129]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f14e130]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. shared store를 도입할 때 두 저장 구조를 각각 확인해야 한다. 같은 사용자가 두 브라우저에서 로그인하면 같은 token 항목을 공유하거나 덮어쓴다. session ID마다 token을 따로 보관해야 하면 그렇게 설계해야 한다. 저장소는 재시작과 replica 이동을 견뎌야 한다. 공유 durable store와 session affinity, 저장 token 암호화 중 무엇을 쓸지 정하고 암호화 key 교체 방법도 같이 정한다. logout에서는 application session과 authorized client를 모두 정리한다. 두 상태의 lookup key가 다르므로 삭제 처리도 각각 확인해야 한다.
- generic [ref=f14e131]:
- button "위로" [ref=f14e132]
- button "아래로" [ref=f14e133]
- button "삭제" [ref=f14e134]
- generic [ref=f14e135]:
- generic [ref=f14e136]:
- generic [ref=f14e137]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f14e138]: downstream 오류를 화면 오류로 바꾸는 규칙을 둔다
- generic [ref=f14e139]:
- generic [ref=f14e140]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f14e141]: Resource Server의 401을 그대로 내려보내면 사용자는 로그인이 끊긴 것인지 권한이 없는 것인지 알 수 없다. timeout과 retry, circuit breaker, 재로그인 전환도 함께 정한다. 모든 UI 요청이 BFF를 지나기 때문에 여기서 정하지 않으면 화면마다 다르게 처리된다.
- generic [ref=f14e142]:
- button "위로" [ref=f14e143]
- button "아래로" [ref=f14e144]
- button "삭제" [ref=f14e145]
- generic [ref=f14e146]:
- generic [ref=f14e147]:
- generic [ref=f14e148]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f14e149]: 자기 보고 값을 증거로 쓰지 않는다
- generic [ref=f14e150]:
- generic [ref=f14e151]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f14e152]: 「브라우저에 token이 없다」고 서버가 응답에 적는 값은 서버가 넣은 상수다. 브라우저를 들여다본 결과가 아니다. 진단 endpoint의 응답과 별개로 브라우저 개발자 도구에서 network 요청과 Web Storage를 직접 확인한다. 애플리케이션이 스스로 보고한 값과 브라우저에서 관측한 결과를 구분해 기록한다.
- generic [ref=f14e153]:
- button "위로" [ref=f14e154]
- button "아래로" [ref=f14e155]
- button "삭제" [ref=f14e156]
- generic [ref=f14e157]:
- generic [ref=f14e158]:
- generic [ref=f14e159]: 규칙 6 제목
- textbox "규칙 6 제목" [ref=f14e160]: BFF에서도 XSS 방어는 별도로 필요하다
- generic [ref=f14e161]:
- generic [ref=f14e162]: 규칙 6 본문
- textbox "규칙 6 본문" [ref=f14e163]: same-origin에서 악성 script가 실행되면 피해자 session으로 BFF endpoint를 호출하고 JavaScript에서 읽을 수 있는 CSRF cookie에도 접근할 수 있다. BFF는 OAuth token 원문을 브라우저 JavaScript에 전달하지 않지만, CSP와 output encoding, 의존성 무결성, 애플리케이션 인가는 별도로 적용해야 한다.
- generic [ref=f14e164]:
- button "위로" [ref=f14e165]
- button "아래로" [disabled] [ref=f14e166]
- button "삭제" [ref=f14e167]
- button "규칙 추가" [ref=f14e168]
- group "적용 조건" [ref=f14e169]:
- generic [ref=f14e171]:
- generic [ref=f14e172]:
- generic [ref=f14e173]: 적용 조건 1
- textbox "적용 조건 1" [ref=f14e174]: 브라우저가 OAuth token을 받아서는 안 될 때
- generic [ref=f14e175]:
- button "위로" [disabled] [ref=f14e176]
- button "아래로" [ref=f14e177]
- button "삭제" [ref=f14e178]
- generic [ref=f14e179]:
- generic [ref=f14e180]:
- generic [ref=f14e181]: 적용 조건 2
- textbox "적용 조건 2" [ref=f14e182]: backend가 화면에 맞춰 여러 API를 조합해야 할 때
- generic [ref=f14e183]:
- button "위로" [ref=f14e184]
- button "아래로" [ref=f14e185]
- button "삭제" [ref=f14e186]
- generic [ref=f14e187]:
- generic [ref=f14e188]:
- generic [ref=f14e189]: 적용 조건 3
- textbox "적용 조건 3" [ref=f14e190]: 로그인 상태를 애플리케이션이 소유해야 할 때
- generic [ref=f14e191]:
- button "위로" [ref=f14e192]
- button "아래로" [ref=f14e193]
- button "삭제" [ref=f14e194]
- generic [ref=f14e195]:
- generic [ref=f14e196]:
- generic [ref=f14e197]: 적용 조건 4
- textbox "적용 조건 4" [ref=f14e198]: downstream API가 늘어나도 브라우저는 하나만 알게 하고 싶을 때
- generic [ref=f14e199]:
- button "위로" [ref=f14e200]
- button "아래로" [disabled] [ref=f14e201]
- button "삭제" [ref=f14e202]
- button "적용 조건 추가" [ref=f14e203]
- group "예외" [ref=f14e204]:
- generic [ref=f14e206]:
- generic [ref=f14e207]:
- generic [ref=f14e208]: 예외 1
- textbox "예외 1" [ref=f14e209]: stateless 직접 API 호출과 독립 client가 핵심이면 BFF를 넣지 않는다. server state와 단일 장애 지점만 늘어난다.
- generic [ref=f14e210]:
- button "위로" [disabled] [ref=f14e211]
- button "아래로" [ref=f14e212]
- button "삭제" [ref=f14e213]
- generic [ref=f14e214]:
- generic [ref=f14e215]:
- generic [ref=f14e216]: 예외 2
- textbox "예외 2" [ref=f14e217]: 브라우저의 직접 API 호출을 남겨야 하면 refresh credential만 서버로 분리하는 구조가 맞다.
- generic [ref=f14e218]:
- button "위로" [ref=f14e219]
- button "아래로" [ref=f14e220]
- button "삭제" [ref=f14e221]
- generic [ref=f14e222]:
- generic [ref=f14e223]:
- generic [ref=f14e224]: 예외 3
- textbox "예외 3" [ref=f14e225]: server state를 둘 수 없는 환경이면 브라우저가 token을 직접 다루는 구조가 더 단순하다.
- generic [ref=f14e226]:
- button "위로" [ref=f14e227]
- button "아래로" [disabled] [ref=f14e228]
- button "삭제" [ref=f14e229]
- button "예외 추가" [ref=f14e230]
- group "예시" [ref=f14e231]:
- generic [ref=f14e233]:
- generic [ref=f14e234]:
- generic [ref=f14e235]: 예시 1
- textbox "예시 1" [ref=f14e236]: 브라우저 요청에는 Authorization 헤더가 없고 session cookie만 있다
- generic [ref=f14e237]:
- button "위로" [disabled] [ref=f14e238]
- button "아래로" [ref=f14e239]
- button "삭제" [ref=f14e240]
- generic [ref=f14e241]:
- generic [ref=f14e242]:
- generic [ref=f14e243]: 예시 2
- textbox "예시 2" [ref=f14e244]: BFF가 authorized client에서 access token을 읽어 downstream Bearer 요청을 새로 만든다
- generic [ref=f14e245]:
- button "위로" [ref=f14e246]
- button "아래로" [ref=f14e247]
- button "삭제" [ref=f14e248]
- generic [ref=f14e249]:
- generic [ref=f14e250]:
- generic [ref=f14e251]: 예시 3
- textbox "예시 3" [ref=f14e252]: CSRF 헤더가 없는 POST는 403이 되고 cookie의 raw 값을 헤더에 넣은 POST는 200이 된다
- generic [ref=f14e253]:
- button "위로" [ref=f14e254]
- button "아래로" [ref=f14e255]
- button "삭제" [ref=f14e256]
- generic [ref=f14e257]:
- generic [ref=f14e258]:
- generic [ref=f14e259]: 예시 4
- textbox "예시 4" [ref=f14e260]: 응답 본문의 token은 가려진 값이고 헤더에 넣는 값은 cookie의 raw 값이다
- generic [ref=f14e261]:
- button "위로" [ref=f14e262]
- button "아래로" [ref=f14e263]
- button "삭제" [ref=f14e264]
- generic [ref=f14e265]:
- generic [ref=f14e266]:
- generic [ref=f14e267]: 예시 5
- textbox "예시 5" [ref=f14e268]: 진단 endpoint의 browserTokenCount는 controller literal이라서 token 비노출의 근거가 아니다
- generic [ref=f14e269]:
- button "위로" [ref=f14e270]
- button "아래로" [disabled] [ref=f14e271]
- button "삭제" [ref=f14e272]
- button "예시 추가" [ref=f14e273]
- generic [ref=f14e274]:
- generic [ref=f14e275]: 마지막 검증일
- textbox "마지막 검증일" [ref=f14e276]
- region [ref=f14e277]:
- generic [ref=f14e278]:
- paragraph [ref=f14e279]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f14e280]
- generic [ref=f14e283]:
- generic [ref=f14e284]:
- navigation "문서 경로" [ref=f14e285]:
- link "Reference" [ref=f14e286] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f14e287]: /
- generic [ref=f14e288]: OAuth/OIDC 인증 경계
- generic [ref=f14e289]: /
- link "KeyCloak Patterns" [ref=f14e290] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "BFF 인증 구조 설계 기준" [level=1] [ref=f14e291]
- paragraph [ref=f14e292]: BFF가 OAuth token을 server-side에서 관리하고 브라우저는 session cookie로 BFF를 호출할 때 필요한 설계 항목을 정리한다. CSRF 검증, authorized client 저장소, logout, downstream 오류 처리가 핵심이다.
- generic [ref=f14e293]:
- generic [ref=f14e294]:
- term [ref=f14e295]: 유형
- definition [ref=f14e296]: Reference
- generic [ref=f14e297]:
- term [ref=f14e298]: 프로젝트
- definition [ref=f14e299]: KeyCloak Patterns
- generic [ref=f14e300]:
- term [ref=f14e301]: 게시
- definition [ref=f14e302]: 게시 전
- region [ref=f14e303]:
- paragraph [ref=f14e304]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f14e305]
- paragraph [ref=f14e306]: BFF 구조에서는 BFF가 authorization code를 token으로 교환하고 access token을 사용해 Resource Server를 호출한다. 따라서 session과 authorized client를 함께 관리하는 보안 구성요소로 본다.cookie가 credential이 되면 브라우저가 요청마다 자동으로 붙인다. 값을 바꾸는 요청은 사용자의 의도인지 따로 확인해야 한다. 그리고 재시작과 replica 이동을 견딜 저장소도 함께 필요해진다.여기 있는 것은 「BFF를 쓴다」로 답이 되지 않는 항목들이다.
- article [ref=f14e307]:
- region [ref=f14e308]:
- heading "판단 기준" [level=2] [ref=f14e309]
- list [ref=f14e310]:
- listitem [ref=f14e311]:
- generic [ref=f14e312]: "01"
- generic [ref=f14e313]:
- heading "브라우저에는 OAuth token을 전달하지 않는다" [level=3] [ref=f14e314]
- paragraph [ref=f14e315]: "access token과 refresh token은 server-side authorized client에 보관한다. 브라우저가 token을 직접 사용할 필요가 없도록 BFF가 downstream 요청의 `Authorization` 헤더를 만든다. session cookie는 downstream으로 전달하지 않는다. BFF가 session을 애플리케이션 credential로 소비하고, Resource Server가 아는 Bearer 요청을 새로 만든다. 두 credential은 같은 요청 처리 안에 있지만 검증하는 주체가 다르다."
- listitem [ref=f14e316]:
- generic [ref=f14e317]: "02"
- generic [ref=f14e318]:
- heading "cookie가 credential이면 상태 변경 요청에 CSRF 검증을 둔다" [level=3] [ref=f14e319]
- paragraph [ref=f14e320]: session cookie는 브라우저가 자동으로 전송하므로 상태 변경 endpoint에는 CSRF 검증을 적용한다. 현재 구성은 JavaScript가 CSRF cookie를 읽어 요청 헤더에 같은 값을 전달하는 방식을 사용한다. 노출 값과 제출 값이 다를 수 있다. 응답 본문의 token이 가려진 값이면 헤더에 넣는 값은 cookie에서 읽어야 한다. 두 값을 같다고 가정하고 구현하면 클라이언트가 그대로 403을 받는다. SameSite와 CSRF token은 역할이 다르다. SameSite는 특정 cross-site 요청에서 cookie 전송을 제한하는 브라우저 정책이고, CSRF token은 cookie가 포함된 상태 변경 요청을 서버가 추가로 검증하는 값이다. 같은 site로 계산되는 다른 origin 요청도 고려해야 한다.
- listitem [ref=f14e321]:
- generic [ref=f14e322]: "03"
- generic [ref=f14e323]:
- heading "session과 authorized client의 수명주기를 따로 설계한다" [level=3] [ref=f14e324]
- paragraph [ref=f14e325]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. shared store를 도입할 때 두 저장 구조를 각각 확인해야 한다. 같은 사용자가 두 브라우저에서 로그인하면 같은 token 항목을 공유하거나 덮어쓴다. session ID마다 token을 따로 보관해야 하면 그렇게 설계해야 한다. 저장소는 재시작과 replica 이동을 견뎌야 한다. 공유 durable store와 session affinity, 저장 token 암호화 중 무엇을 쓸지 정하고 암호화 key 교체 방법도 같이 정한다. logout에서는 application session과 authorized client를 모두 정리한다. 두 상태의 lookup key가 다르므로 삭제 처리도 각각 확인해야 한다.
- listitem [ref=f14e326]:
- generic [ref=f14e327]: "04"
- generic [ref=f14e328]:
- heading "downstream 오류를 화면 오류로 바꾸는 규칙을 둔다" [level=3] [ref=f14e329]
- paragraph [ref=f14e330]: Resource Server의 401을 그대로 내려보내면 사용자는 로그인이 끊긴 것인지 권한이 없는 것인지 알 수 없다. timeout과 retry, circuit breaker, 재로그인 전환도 함께 정한다. 모든 UI 요청이 BFF를 지나기 때문에 여기서 정하지 않으면 화면마다 다르게 처리된다.
- listitem [ref=f14e331]:
- generic [ref=f14e332]: "05"
- generic [ref=f14e333]:
- heading "자기 보고 값을 증거로 쓰지 않는다" [level=3] [ref=f14e334]
- paragraph [ref=f14e335]: 「브라우저에 token이 없다」고 서버가 응답에 적는 값은 서버가 넣은 상수다. 브라우저를 들여다본 결과가 아니다. 진단 endpoint의 응답과 별개로 브라우저 개발자 도구에서 network 요청과 Web Storage를 직접 확인한다. 애플리케이션이 스스로 보고한 값과 브라우저에서 관측한 결과를 구분해 기록한다.
- listitem [ref=f14e336]:
- generic [ref=f14e337]: "06"
- generic [ref=f14e338]:
- heading "BFF에서도 XSS 방어는 별도로 필요하다" [level=3] [ref=f14e339]
- paragraph [ref=f14e340]: same-origin에서 악성 script가 실행되면 피해자 session으로 BFF endpoint를 호출하고 JavaScript에서 읽을 수 있는 CSRF cookie에도 접근할 수 있다. BFF는 OAuth token 원문을 브라우저 JavaScript에 전달하지 않지만, CSP와 output encoding, 의존성 무결성, 애플리케이션 인가는 별도로 적용해야 한다.
- region [ref=f14e341]:
- heading "적용할 때" [level=2] [ref=f14e342]
- list [ref=f14e343]:
- listitem [ref=f14e344]: 브라우저가 OAuth token을 받아서는 안 될 때
- listitem [ref=f14e345]: backend가 화면에 맞춰 여러 API를 조합해야 할 때
- listitem [ref=f14e346]: 로그인 상태를 애플리케이션이 소유해야 할 때
- listitem [ref=f14e347]: downstream API가 늘어나도 브라우저는 하나만 알게 하고 싶을 때
- region [ref=f14e348]:
- heading "예외와 주의" [level=2] [ref=f14e349]
- list [ref=f14e350]:
- listitem [ref=f14e351]: stateless 직접 API 호출과 독립 client가 핵심이면 BFF를 넣지 않는다. server state와 단일 장애 지점만 늘어난다.
- listitem [ref=f14e352]: 브라우저의 직접 API 호출을 남겨야 하면 refresh credential만 서버로 분리하는 구조가 맞다.
- listitem [ref=f14e353]: server state를 둘 수 없는 환경이면 브라우저가 token을 직접 다루는 구조가 더 단순하다.
- region [ref=f14e354]:
- heading "예시" [level=2] [ref=f14e355]
- list [ref=f14e356]:
- listitem [ref=f14e357]: 브라우저 요청에는 Authorization 헤더가 없고 session cookie만 있다
- listitem [ref=f14e358]: BFF가 authorized client에서 access token을 읽어 downstream Bearer 요청을 새로 만든다
- listitem [ref=f14e359]: CSRF 헤더가 없는 POST는 403이 되고 cookie의 raw 값을 헤더에 넣은 POST는 200이 된다
- listitem [ref=f14e360]: 응답 본문의 token은 가려진 값이고 헤더에 넣는 값은 cookie의 raw 값이다
- listitem [ref=f14e361]: 진단 endpoint의 browserTokenCount는 controller literal이라서 token 비노출의 근거가 아니다
- paragraph [ref=f14e362]: 마지막 검증
- region [ref=f14e363]:
- paragraph [ref=f14e364]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f14e365]
- list [ref=f14e366]:
- listitem [ref=f14e367]:
- link "이 기준의 항목 중 실제로 구현된 것과 비어 있는 것을 센 기록이다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f14e368] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f14e369]: 이 기준의 항목 중 실제로 구현된 것과 비어 있는 것을 센 기록이다.
- strong [ref=f14e370]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f14e371]:
- complementary [ref=f14e372]:
- heading "작업 상태" [level=2] [ref=f14e373]
- status "편집 상태" [ref=f14e374]: 저장됨
- generic [ref=f14e375]:
- generic [ref=f14e376]:
- term [ref=f14e377]: 저장 버전
- definition [ref=f14e378]: "11"
- generic [ref=f14e379]:
- term [ref=f14e380]: 종류
- definition [ref=f14e381]: REFERENCE
- paragraph [ref=f14e382]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f14e383]:
- button "저장" [disabled] [ref=f14e384]
- button "게시" [ref=f14e385]
- paragraph [ref=f14e386]: 버전 11으로 저장했습니다.
@@ -0,0 +1,486 @@
- generic [ref=f15e3]:
- link "본문으로 건너뛰기" [ref=f15e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f15e5]:
- generic [ref=f15e6]:
- link "TechLog Studio" [ref=f15e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f15e8]: Studio
- navigation "Studio 주 탐색" [ref=f15e10]:
- link "작업본" [ref=f15e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f15e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f15e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f15e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f15e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f15e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f15e17]
- main [ref=f15e18]:
- generic [ref=f15e19]:
- generic [ref=f15e20]:
- region [ref=f15e21]:
- generic [ref=f15e22]:
- paragraph [ref=f15e23]: REFERENCE · VERSION 11
- heading "문서 편집" [level=1] [ref=f15e24]
- paragraph [ref=f15e25]: OAuth/OIDC 인증 패턴 선택 기준
- region [ref=f15e26]:
- generic [ref=f15e27]:
- paragraph [ref=f15e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f15e29]
- generic [ref=f15e30]:
- generic [ref=f15e31]:
- generic [ref=f15e32]: 제목
- textbox "제목" [ref=f15e33]: OAuth/OIDC 인증 패턴 선택 기준
- generic [ref=f15e34]:
- generic [ref=f15e35]: slug
- textbox "slug" [ref=f15e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: oauth-oidc-pattern-selection-criteria
- generic [ref=f15e37]:
- generic [ref=f15e38]: 요약
- textbox "요약" [ref=f15e39]: SPA, Mediator, BFF, OAuth2-Proxy는 브라우저의 access token 사용 여부, Resource Server 호출 주체, server-side 인증 상태, 보호 자원이 검증하는 credential, CSRF 처리 위치가 서로 다르다. 패턴 선택에서는 이 다섯 항목을 요구사항과 운영 환경에 맞춰 비교한다.
- generic [ref=f15e40]:
- generic [ref=f15e41]: Topic
- combobox "Topic" [ref=f15e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f15e43]:
- generic [ref=f15e44]: Project
- combobox "Project" [ref=f15e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f15e46]:
- generic [ref=f15e48]:
- generic [ref=f15e49]:
- generic [ref=f15e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f15e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [disabled]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f15e52]:
- generic [ref=f15e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f15e54]: 브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다.
- generic [ref=f15e55]:
- button "위로" [disabled] [ref=f15e56]
- button "아래로" [ref=f15e57]
- button "삭제" [ref=f15e58]
- generic [ref=f15e59]:
- generic [ref=f15e60]:
- generic [ref=f15e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f15e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [disabled]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f15e63]:
- generic [ref=f15e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f15e65]: mediator가 refresh token을 관리하고 브라우저가 access token으로 API를 직접 호출하는 구성을 확인했다.
- generic [ref=f15e66]:
- button "위로" [ref=f15e67]
- button "아래로" [ref=f15e68]
- button "삭제" [ref=f15e69]
- generic [ref=f15e70]:
- generic [ref=f15e71]:
- generic [ref=f15e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f15e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [disabled]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f15e74]:
- generic [ref=f15e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f15e76]: BFF가 code 교환, token 보관, Resource Server 호출을 모두 처리하는 구성을 확인했다.
- generic [ref=f15e77]:
- button "위로" [ref=f15e78]
- button "아래로" [ref=f15e79]
- button "삭제" [ref=f15e80]
- generic [ref=f15e81]:
- generic [ref=f15e82]:
- generic [ref=f15e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f15e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [disabled]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f15e85]:
- generic [ref=f15e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f15e87]: 인증이 edge로 가면 보호 자원이 검증하는 것이 JWT에서 헤더로 바뀐다.
- generic [ref=f15e88]:
- button "위로" [ref=f15e89]
- button "아래로" [ref=f15e90]
- button "삭제" [ref=f15e91]
- generic [ref=f15e92]:
- generic [ref=f15e93]:
- generic [ref=f15e94]: 관계 5 대상
- combobox "관계 5 대상" [ref=f15e95]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" [selected]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f15e96]:
- generic [ref=f15e97]: 관계 5 이유
- textbox "관계 5 이유" [ref=f15e98]: 이 기준의 첫 항목을 프로젝트 결정으로 굳힌 기록이다.
- generic [ref=f15e99]:
- button "위로" [ref=f15e100]
- button "아래로" [disabled] [ref=f15e101]
- button "삭제" [ref=f15e102]
- button "관계 추가" [ref=f15e103]
- region [ref=f15e104]:
- generic [ref=f15e105]:
- paragraph [ref=f15e106]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f15e107]
- generic [ref=f15e108]:
- generic [ref=f15e109]: 목적
- textbox "목적" [ref=f15e110]: 브라우저에 token이 덜 보이는 순서는 있다. 그 순서를 보안 등급으로 쓰면 판단이 틀린다. BFF는 브라우저 token을 없애지만 server session과 공유 저장소를 만든다. Forward-Auth는 애플리케이션의 token custody를 줄이지만 edge 헤더 신뢰와 network 경계를 만든다. 새로 생긴 쪽을 감당할 수 없는 환경이면 앞 구조가 더 안전하다. 번호가 아니라 배치를 본다.
- group "규칙" [ref=f15e111]:
- generic [ref=f15e113]:
- generic [ref=f15e114]:
- generic [ref=f15e115]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f15e116]: 다섯 항목으로 구조를 비교한다
- generic [ref=f15e117]:
- generic [ref=f15e118]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f15e119]: "구조를 비교할 때는 브라우저 token 전달, Resource Server 호출 주체, server-side 상태, Resource Server의 검증 대상, CSRF 처리 위치를 확인한다. 브라우저가 access token을 받나 SPA : o Mediator : o BFF : x Forward-Auth : x 브라우저가 보호 자원을 직접 부르나 SPA : o Mediator : o BFF : x Forward-Auth : x server-side token 상태가 있나 SPA : x Mediator : o BFF : o Forward-Auth : proxy session 보호 자원이 무엇을 검증하나 SPA : 서명된 JWT Mediator : 서명된 JWT BFF : 서명된 JWT Forward-Auth : edge가 붙인 헤더 cookie가 credential이면 CSRF 검증이 어디에 붙나 SPA : 해당 없음 Mediator : session endpoint BFF : 상태 변경 endpoint Forward-Auth : proxy cookie 기준 호출 주체와 credential 저장 방식을 정한 뒤에는 401/403, token 갱신 실패, logout을 어느 계층에서 처리할지 정한다."
- generic [ref=f15e120]:
- button "위로" [disabled] [ref=f15e121]
- button "아래로" [ref=f15e122]
- button "삭제" [ref=f15e123]
- generic [ref=f15e124]:
- generic [ref=f15e125]:
- generic [ref=f15e126]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f15e127]: 피해야 할 조건을 먼저 확인한다
- generic [ref=f15e128]:
- generic [ref=f15e129]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f15e130]: 정책상 브라우저에 token을 둘 수 없으면 memory에만 두는 보관은 답이 아니다. backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없으면 edge에 인증을 맡기지 않는다. 이 조건에 걸리면 다른 항목은 볼 필요가 없다.
- generic [ref=f15e131]:
- button "위로" [ref=f15e132]
- button "아래로" [ref=f15e133]
- button "삭제" [ref=f15e134]
- generic [ref=f15e135]:
- generic [ref=f15e136]:
- generic [ref=f15e137]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f15e138]: 선택 조건과 운영 부담을 함께 기록한다
- generic [ref=f15e139]:
- generic [ref=f15e140]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f15e141]: 선택 결과만 적지 않고 어떤 요구에서 해당 패턴을 선택했는지와 적용하기 어려운 조건도 함께 기록한다.
- generic [ref=f15e142]:
- button "위로" [ref=f15e143]
- button "아래로" [ref=f15e144]
- button "삭제" [ref=f15e145]
- generic [ref=f15e146]:
- generic [ref=f15e147]:
- generic [ref=f15e148]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f15e149]: 이름으로 운영 속성을 추정하지 않는다
- generic [ref=f15e150]:
- generic [ref=f15e151]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f15e152]: BFF나 forward-auth라는 이름은 배치를 말할 뿐이다. 공유 저장소와 장애 복구, session failover, secret 교체가 갖춰져 있는지는 매번 따로 확인한다.
- generic [ref=f15e153]:
- button "위로" [ref=f15e154]
- button "아래로" [ref=f15e155]
- button "삭제" [ref=f15e156]
- generic [ref=f15e157]:
- generic [ref=f15e158]:
- generic [ref=f15e159]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f15e160]: 옮기는 것은 업그레이드가 아니다
- generic [ref=f15e161]:
- generic [ref=f15e162]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f15e163]: 패턴을 바꾸면 credential을 저장하고 전달하고 검증하는 주체도 함께 바뀐다. edge header가 계속 늘어나 애플리케이션 도메인 정보까지 전달해야 한다면 BFF에서 인가와 API 조합을 처리하는 구성을 다시 검토할 수 있다.
- generic [ref=f15e164]:
- button "위로" [ref=f15e165]
- button "아래로" [disabled] [ref=f15e166]
- button "삭제" [ref=f15e167]
- button "규칙 추가" [ref=f15e168]
- group "적용 조건" [ref=f15e169]:
- generic [ref=f15e171]:
- generic [ref=f15e172]:
- generic [ref=f15e173]: 적용 조건 1
- textbox "적용 조건 1" [ref=f15e174]: 인증 구조를 처음 고를 때
- generic [ref=f15e175]:
- button "위로" [disabled] [ref=f15e176]
- button "아래로" [ref=f15e177]
- button "삭제" [ref=f15e178]
- generic [ref=f15e179]:
- generic [ref=f15e180]:
- generic [ref=f15e181]: 적용 조건 2
- textbox "적용 조건 2" [ref=f15e182]: 한 구조에서 다른 구조로 옮기려 할 때
- generic [ref=f15e183]:
- button "위로" [ref=f15e184]
- button "아래로" [ref=f15e185]
- button "삭제" [ref=f15e186]
- generic [ref=f15e187]:
- generic [ref=f15e188]:
- generic [ref=f15e189]: 적용 조건 3
- textbox "적용 조건 3" [ref=f15e190]: 구조를 문서로 비교할 때
- generic [ref=f15e191]:
- button "위로" [ref=f15e192]
- button "아래로" [ref=f15e193]
- button "삭제" [ref=f15e194]
- generic [ref=f15e195]:
- generic [ref=f15e196]:
- generic [ref=f15e197]: 적용 조건 4
- textbox "적용 조건 4" [ref=f15e198]: 이름만 보고 고른 구조를 다시 검토할 때
- generic [ref=f15e199]:
- button "위로" [ref=f15e200]
- button "아래로" [disabled] [ref=f15e201]
- button "삭제" [ref=f15e202]
- button "적용 조건 추가" [ref=f15e203]
- group "예외" [ref=f15e204]:
- generic [ref=f15e206]:
- generic [ref=f15e207]:
- generic [ref=f15e208]: 예외 1
- textbox "예외 1" [ref=f15e209]: 요구가 하나로 좁혀지면 비교가 필요 없다. 브라우저에 token을 둘 수 없고 backend가 API를 조합해야 하면 선택지는 하나다.
- generic [ref=f15e210]:
- button "위로" [disabled] [ref=f15e211]
- button "아래로" [ref=f15e212]
- button "삭제" [ref=f15e213]
- generic [ref=f15e214]:
- generic [ref=f15e215]:
- generic [ref=f15e216]: 예외 2
- textbox "예외 2" [ref=f15e217]: 학습이나 시연이 목적이면 운영 속성 비교를 하지 않아도 된다. 그때는 학습 환경이라고 문서에 적어 둔다.
- generic [ref=f15e218]:
- button "위로" [ref=f15e219]
- button "아래로" [disabled] [ref=f15e220]
- button "삭제" [ref=f15e221]
- button "예외 추가" [ref=f15e222]
- group "예시" [ref=f15e223]:
- generic [ref=f15e225]:
- generic [ref=f15e226]:
- generic [ref=f15e227]: 예시 1
- textbox "예시 1" [ref=f15e228]: "SPA : 브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다"
- generic [ref=f15e229]:
- button "위로" [disabled] [ref=f15e230]
- button "아래로" [ref=f15e231]
- button "삭제" [ref=f15e232]
- generic [ref=f15e233]:
- generic [ref=f15e234]:
- generic [ref=f15e235]: 예시 2
- textbox "예시 2" [ref=f15e236]: "Mediator : refresh token은 server에 있고 access token은 응답 본문으로 브라우저에 간다"
- generic [ref=f15e237]:
- button "위로" [ref=f15e238]
- button "아래로" [ref=f15e239]
- button "삭제" [ref=f15e240]
- generic [ref=f15e241]:
- generic [ref=f15e242]:
- generic [ref=f15e243]: 예시 3
- textbox "예시 3" [ref=f15e244]: "BFF : server가 code 교환·token 관리·API 호출을 담당하고 브라우저는 session cookie로 BFF를 호출한다"
- generic [ref=f15e245]:
- button "위로" [ref=f15e246]
- button "아래로" [ref=f15e247]
- button "삭제" [ref=f15e248]
- generic [ref=f15e249]:
- generic [ref=f15e250]:
- generic [ref=f15e251]: 예시 4
- textbox "예시 4" [ref=f15e252]: "Forward-Auth : edge가 인증하고 upstream은 edge가 붙인 헤더를 본다"
- generic [ref=f15e253]:
- button "위로" [ref=f15e254]
- button "아래로" [disabled] [ref=f15e255]
- button "삭제" [ref=f15e256]
- button "예시 추가" [ref=f15e257]
- generic [ref=f15e258]:
- generic [ref=f15e259]: 마지막 검증일
- textbox "마지막 검증일" [ref=f15e260]
- region [ref=f15e261]:
- generic [ref=f15e262]:
- paragraph [ref=f15e263]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f15e264]
- generic [ref=f15e267]:
- generic [ref=f15e268]:
- navigation "문서 경로" [ref=f15e269]:
- link "Reference" [ref=f15e270] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f15e271]: /
- generic [ref=f15e272]: OAuth/OIDC 인증 경계
- generic [ref=f15e273]: /
- link "KeyCloak Patterns" [ref=f15e274] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "OAuth/OIDC 인증 패턴 선택 기준" [level=1] [ref=f15e275]
- paragraph [ref=f15e276]: SPA, Mediator, BFF, OAuth2-Proxy는 브라우저의 access token 사용 여부, Resource Server 호출 주체, server-side 인증 상태, 보호 자원이 검증하는 credential, CSRF 처리 위치가 서로 다르다. 패턴 선택에서는 이 다섯 항목을 요구사항과 운영 환경에 맞춰 비교한다.
- generic [ref=f15e277]:
- generic [ref=f15e278]:
- term [ref=f15e279]: 유형
- definition [ref=f15e280]: Reference
- generic [ref=f15e281]:
- term [ref=f15e282]: 프로젝트
- definition [ref=f15e283]: KeyCloak Patterns
- generic [ref=f15e284]:
- term [ref=f15e285]: 게시
- definition [ref=f15e286]: 게시 전
- region [ref=f15e287]:
- paragraph [ref=f15e288]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f15e289]
- paragraph [ref=f15e290]: 브라우저에 token이 덜 보이는 순서는 있다. 그 순서를 보안 등급으로 쓰면 판단이 틀린다.BFF는 브라우저 token을 없애지만 server session과 공유 저장소를 만든다. Forward-Auth는 애플리케이션의 token custody를 줄이지만 edge 헤더 신뢰와 network 경계를 만든다. 새로 생긴 쪽을 감당할 수 없는 환경이면 앞 구조가 더 안전하다.번호가 아니라 배치를 본다.
- article [ref=f15e291]:
- region [ref=f15e292]:
- heading "판단 기준" [level=2] [ref=f15e293]
- list [ref=f15e294]:
- listitem [ref=f15e295]:
- generic [ref=f15e296]: "01"
- generic [ref=f15e297]:
- heading "다섯 항목으로 구조를 비교한다" [level=3] [ref=f15e298]
- paragraph [ref=f15e299]: "구조를 비교할 때는 브라우저 token 전달, Resource Server 호출 주체, server-side 상태, Resource Server의 검증 대상, CSRF 처리 위치를 확인한다. 브라우저가 access token을 받나 SPA : o Mediator : o BFF : x Forward-Auth : x 브라우저가 보호 자원을 직접 부르나 SPA : o Mediator : o BFF : x Forward-Auth : x server-side token 상태가 있나 SPA : x Mediator : o BFF : o Forward-Auth : proxy session 보호 자원이 무엇을 검증하나 SPA : 서명된 JWT Mediator : 서명된 JWT BFF : 서명된 JWT Forward-Auth : edge가 붙인 헤더 cookie가 credential이면 CSRF 검증이 어디에 붙나 SPA : 해당 없음 Mediator : session endpoint BFF : 상태 변경 endpoint Forward-Auth : proxy cookie 기준 호출 주체와 credential 저장 방식을 정한 뒤에는 401/403, token 갱신 실패, logout을 어느 계층에서 처리할지 정한다."
- listitem [ref=f15e300]:
- generic [ref=f15e301]: "02"
- generic [ref=f15e302]:
- heading "피해야 할 조건을 먼저 확인한다" [level=3] [ref=f15e303]
- paragraph [ref=f15e304]: 정책상 브라우저에 token을 둘 수 없으면 memory에만 두는 보관은 답이 아니다. backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없으면 edge에 인증을 맡기지 않는다. 이 조건에 걸리면 다른 항목은 볼 필요가 없다.
- listitem [ref=f15e305]:
- generic [ref=f15e306]: "03"
- generic [ref=f15e307]:
- heading "선택 조건과 운영 부담을 함께 기록한다" [level=3] [ref=f15e308]
- paragraph [ref=f15e309]: 선택 결과만 적지 않고 어떤 요구에서 해당 패턴을 선택했는지와 적용하기 어려운 조건도 함께 기록한다.
- listitem [ref=f15e310]:
- generic [ref=f15e311]: "04"
- generic [ref=f15e312]:
- heading "이름으로 운영 속성을 추정하지 않는다" [level=3] [ref=f15e313]
- paragraph [ref=f15e314]: BFF나 forward-auth라는 이름은 배치를 말할 뿐이다. 공유 저장소와 장애 복구, session failover, secret 교체가 갖춰져 있는지는 매번 따로 확인한다.
- listitem [ref=f15e315]:
- generic [ref=f15e316]: "05"
- generic [ref=f15e317]:
- heading "옮기는 것은 업그레이드가 아니다" [level=3] [ref=f15e318]
- paragraph [ref=f15e319]: 패턴을 바꾸면 credential을 저장하고 전달하고 검증하는 주체도 함께 바뀐다. edge header가 계속 늘어나 애플리케이션 도메인 정보까지 전달해야 한다면 BFF에서 인가와 API 조합을 처리하는 구성을 다시 검토할 수 있다.
- region [ref=f15e320]:
- heading "적용할 때" [level=2] [ref=f15e321]
- list [ref=f15e322]:
- listitem [ref=f15e323]: 인증 구조를 처음 고를 때
- listitem [ref=f15e324]: 한 구조에서 다른 구조로 옮기려 할 때
- listitem [ref=f15e325]: 구조를 문서로 비교할 때
- listitem [ref=f15e326]: 이름만 보고 고른 구조를 다시 검토할 때
- region [ref=f15e327]:
- heading "예외와 주의" [level=2] [ref=f15e328]
- list [ref=f15e329]:
- listitem [ref=f15e330]: 요구가 하나로 좁혀지면 비교가 필요 없다. 브라우저에 token을 둘 수 없고 backend가 API를 조합해야 하면 선택지는 하나다.
- listitem [ref=f15e331]: 학습이나 시연이 목적이면 운영 속성 비교를 하지 않아도 된다. 그때는 학습 환경이라고 문서에 적어 둔다.
- region [ref=f15e332]:
- heading "예시" [level=2] [ref=f15e333]
- list [ref=f15e334]:
- listitem [ref=f15e335]: "SPA : 브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다"
- listitem [ref=f15e336]: "Mediator : refresh token은 server에 있고 access token은 응답 본문으로 브라우저에 간다"
- listitem [ref=f15e337]: "BFF : server가 code 교환·token 관리·API 호출을 담당하고 브라우저는 session cookie로 BFF를 호출한다"
- listitem [ref=f15e338]: "Forward-Auth : edge가 인증하고 upstream은 edge가 붙인 헤더를 본다"
- paragraph [ref=f15e339]: 마지막 검증
- region [ref=f15e340]:
- paragraph [ref=f15e341]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f15e342]
- list [ref=f15e343]:
- listitem [ref=f15e344]:
- link "브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f15e345] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- generic [ref=f15e346]: 브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다.
- strong [ref=f15e347]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
- generic [ref=f15e348]:
- listitem [ref=f15e349]:
- link "mediator가 refresh token을 관리하고 브라우저가 access token으로 API를 직접 호출하는 구성을 확인했다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f15e350] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f15e351]: mediator가 refresh token을 관리하고 브라우저가 access token으로 API를 직접 호출하는 구성을 확인했다.
- strong [ref=f15e352]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f15e353]:
- listitem [ref=f15e354]:
- link "BFF가 code 교환, token 보관, Resource Server 호출을 모두 처리하는 구성을 확인했다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f15e355] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f15e356]: BFF가 code 교환, token 보관, Resource Server 호출을 모두 처리하는 구성을 확인했다.
- strong [ref=f15e357]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f15e358]:
- listitem [ref=f15e359]:
- link "인증이 edge로 가면 보호 자원이 검증하는 것이 JWT에서 헤더로 바뀐다. Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f15e360] [cursor=pointer]:
- /url: /cases/identity-header-trust
- generic [ref=f15e361]: 인증이 edge로 가면 보호 자원이 검증하는 것이 JWT에서 헤더로 바뀐다.
- strong [ref=f15e362]: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
- generic [ref=f15e363]:
- complementary [ref=f15e364]:
- heading "작업 상태" [level=2] [ref=f15e365]
- status "편집 상태" [ref=f15e366]: 저장됨
- generic [ref=f15e367]:
- generic [ref=f15e368]:
- term [ref=f15e369]: 저장 버전
- definition [ref=f15e370]: "11"
- generic [ref=f15e371]:
- term [ref=f15e372]: 종류
- definition [ref=f15e373]: REFERENCE
- paragraph [ref=f15e374]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f15e375]:
- button "저장" [disabled] [ref=f15e376]
- button "게시" [ref=f15e377]
- paragraph [ref=f15e378]: 버전 11으로 저장했습니다.
@@ -0,0 +1,515 @@
- generic [ref=f16e3]:
- link "본문으로 건너뛰기" [ref=f16e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f16e5]:
- generic [ref=f16e6]:
- link "TechLog Studio" [ref=f16e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f16e8]: Studio
- navigation "Studio 주 탐색" [ref=f16e10]:
- link "작업본" [ref=f16e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f16e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f16e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f16e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f16e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f16e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f16e17]
- main [ref=f16e18]:
- generic [ref=f16e19]:
- generic [ref=f16e20]:
- region [ref=f16e21]:
- generic [ref=f16e22]:
- paragraph [ref=f16e23]: REFERENCE · VERSION 12
- heading "문서 편집" [level=1] [ref=f16e24]
- paragraph [ref=f16e25]: OAuth Token과 Application Session을 구분하는 기준
- region [ref=f16e26]:
- generic [ref=f16e27]:
- paragraph [ref=f16e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f16e29]
- generic [ref=f16e30]:
- generic [ref=f16e31]:
- generic [ref=f16e32]: 제목
- textbox "제목" [ref=f16e33]: OAuth Token과 Application Session을 구분하는 기준
- generic [ref=f16e34]:
- generic [ref=f16e35]: slug
- textbox "slug" [ref=f16e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: oauth-token-application-session-boundary
- generic [ref=f16e37]:
- generic [ref=f16e38]: 요약
- textbox "요약" [ref=f16e39]: IdP의 SSO session, access token, refresh token, 애플리케이션 session cookie, proxy session cookie는 만든 주체도 소비자도 수명도 다르다. 다섯을 로그인 상태 하나로 부르면 무엇이 만료됐고 무엇을 지워야 하는지 말할 수 없게 된다.
- generic [ref=f16e40]:
- generic [ref=f16e41]: Topic
- combobox "Topic" [ref=f16e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f16e43]:
- generic [ref=f16e44]: Project
- combobox "Project" [ref=f16e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f16e46]:
- generic [ref=f16e48]:
- generic [ref=f16e49]:
- generic [ref=f16e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f16e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f16e52]:
- generic [ref=f16e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f16e54]: JavaScript memory의 OAuth token과 Keycloak SSO session을 구분한 Case다.
- generic [ref=f16e55]:
- button "위로" [disabled] [ref=f16e56]
- button "아래로" [ref=f16e57]
- button "삭제" [ref=f16e58]
- generic [ref=f16e59]:
- generic [ref=f16e60]:
- generic [ref=f16e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f16e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f16e63]:
- generic [ref=f16e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f16e65]: 같은 요청 안에서 session cookie와 access token이 함께 움직인다.
- generic [ref=f16e66]:
- button "위로" [ref=f16e67]
- button "아래로" [ref=f16e68]
- button "삭제" [ref=f16e69]
- generic [ref=f16e70]:
- generic [ref=f16e71]:
- generic [ref=f16e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f16e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [disabled]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f16e74]:
- generic [ref=f16e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f16e76]: BFF에서는 session cookie, JavaScript가 읽는 CSRF token, server-side OAuth token을 각각 다른 용도로 사용한다.
- generic [ref=f16e77]:
- button "위로" [ref=f16e78]
- button "아래로" [ref=f16e79]
- button "삭제" [ref=f16e80]
- generic [ref=f16e81]:
- generic [ref=f16e82]:
- generic [ref=f16e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f16e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [selected]
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f16e85]:
- generic [ref=f16e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f16e87]: Forward-Auth에서는 upstream이 JWT를 직접 검증하지 않고 proxy session을 기반으로 edge가 만든 identity header를 사용한다.
- generic [ref=f16e88]:
- button "위로" [ref=f16e89]
- button "아래로" [disabled] [ref=f16e90]
- button "삭제" [ref=f16e91]
- button "관계 추가" [ref=f16e92]
- region [ref=f16e93]:
- generic [ref=f16e94]:
- paragraph [ref=f16e95]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f16e96]
- generic [ref=f16e97]:
- generic [ref=f16e98]: 목적
- textbox "목적" [ref=f16e99]: 네 구조를 다 실행해 보면 응답에는 모두 같은 사용자 이름이 나오게 되어서 같은 인증 정보라고 묶기 쉽다. 그런데 값이 들어온 곳을 따라가 보면 어떤 때는 JWT 안의 claim이고 어떤 때는 proxy가 만든 헤더다. 둘을 다 로그인 상태라고 부르게 되면 서명을 검증한 것인지 헤더를 확인한 것인지 문장만 봐서는 구분할 수 없게 된다. 로그아웃과 만료 처리는 credential마다 다르다. 어떤 상태를 삭제하거나 만료시킬지 정하려면 IdP SSO session, OAuth token, application session을 구분해서 다뤄야 한다.
- group "규칙" [ref=f16e100]:
- generic [ref=f16e102]:
- generic [ref=f16e103]:
- generic [ref=f16e104]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f16e105]: 다섯 상태에 각각 다른 이름을 쓴다
- generic [ref=f16e106]:
- generic [ref=f16e107]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f16e108]: "IdP SSO session, OAuth access token, OAuth refresh token, 애플리케이션 session cookie, proxy session cookie는 서로 다른 것이라서 문서와 코드, 로그에서 같은 이름을 돌려 쓰지 않는다. 로그와 진단 정보에서도 `로그인 상태`라는 표현만 쓰지 않고 실제 session 또는 token 종류를 기록한다."
- generic [ref=f16e109]:
- button "위로" [disabled] [ref=f16e110]
- button "아래로" [ref=f16e111]
- button "삭제" [ref=f16e112]
- generic [ref=f16e113]:
- generic [ref=f16e114]:
- generic [ref=f16e115]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f16e116]: 만든 주체와 주된 소비자로 구분한다
- generic [ref=f16e117]:
- generic [ref=f16e118]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f16e119]: access token은 IdP가 만들고 Resource Server가 소비하게 되고, 애플리케이션 session cookie는 애플리케이션이 만들어 자기 로그인 상태를 찾는 데 쓰게 되며, proxy session cookie는 proxy의 auth endpoint에만 제시된다. 화면에 같은 사용자 이름이 보이더라도 credential을 발급한 주체와 검증하는 주체가 다르면 별도의 상태로 다룬다.
- generic [ref=f16e120]:
- button "위로" [ref=f16e121]
- button "아래로" [ref=f16e122]
- button "삭제" [ref=f16e123]
- generic [ref=f16e124]:
- generic [ref=f16e125]:
- generic [ref=f16e126]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f16e127]: cookie가 token을 담고 있다고 쓰지 않는다
- generic [ref=f16e128]:
- generic [ref=f16e129]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f16e130]: 애플리케이션 session cookie는 server-side 상태를 찾는 열쇠다. 실제 access token과 refresh token은 별도 store에 있어서 cookie 안에는 없다. proxy session cookie는 같은 모델이 아니다. 서버에 상태를 두지 않고 최소 정보를 cookie 자체에 담아 proxy가 검증하는 구성일 수 있다. 두 cookie를 같은 문장으로 설명하지 않는다. cookie를 token map의 직렬화라고 설명하게 되면 구현 설명이 틀리게 되고, 그 store를 어디에 둘지가 별도 문제라는 것도 함께 가려지게 된다.
- generic [ref=f16e131]:
- button "위로" [ref=f16e132]
- button "아래로" [ref=f16e133]
- button "삭제" [ref=f16e134]
- generic [ref=f16e135]:
- generic [ref=f16e136]:
- generic [ref=f16e137]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f16e138]: 브라우저에 없다는 말의 대상을 밝힌다
- generic [ref=f16e139]:
- generic [ref=f16e140]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f16e141]: 브라우저 JavaScript에 OAuth token을 전달하지 않는 구조에서도 인증 상태는 존재한다. BFF의 HttpOnly session cookie나 IdP 도메인의 SSO cookie는 각각 별도로 유지될 수 있다. 무엇이 없는지를 적지 않으면 브라우저에 인증 상태가 아예 없다는 뜻으로 읽힌다.
- generic [ref=f16e142]:
- button "위로" [ref=f16e143]
- button "아래로" [ref=f16e144]
- button "삭제" [ref=f16e145]
- generic [ref=f16e146]:
- generic [ref=f16e147]:
- generic [ref=f16e148]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f16e149]: 영구 저장소에 없는 것과 실행 중에 없는 것을 나눈다
- generic [ref=f16e150]:
- generic [ref=f16e151]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f16e152]: OAuth token을 JavaScript memory에만 보관하면 Web Storage에 지속적으로 저장하지는 않는다. 실행 중 같은 origin의 script가 응답이나 지역 변수에 접근하는 문제는 별도다. 두 문장을 같은 증거로 쓰게 되면 XSS 위험이 줄었다는 잘못된 결론이 나오게 된다.
- generic [ref=f16e153]:
- button "위로" [ref=f16e154]
- button "아래로" [ref=f16e155]
- button "삭제" [ref=f16e156]
- generic [ref=f16e157]:
- generic [ref=f16e158]:
- generic [ref=f16e159]: 규칙 6 제목
- textbox "규칙 6 제목" [ref=f16e160]: 로그아웃 범위를 상태별로 적는다
- generic [ref=f16e161]:
- generic [ref=f16e162]: 규칙 6 본문
- textbox "규칙 6 본문" [ref=f16e163]: 애플리케이션 상태를 지우는 것과 IdP session을 끝내는 것은 다르고, 이미 발급된 self-contained JWT는 만료 전까지 API에서 계속 통하게 된다. self-contained JWT를 stateless하게 검증하면서 denylist나 introspection을 사용하지 않는 구성에서는 애플리케이션 logout만으로 이미 발급된 access token을 즉시 무효화할 수 없다. 이 경우 짧은 access token TTL을 사용해 유효 시간을 제한한다.
- generic [ref=f16e164]:
- button "위로" [ref=f16e165]
- button "아래로" [ref=f16e166]
- button "삭제" [ref=f16e167]
- generic [ref=f16e168]:
- generic [ref=f16e169]:
- generic [ref=f16e170]: 규칙 7 제목
- textbox "규칙 7 제목" [ref=f16e171]: Logout 대상 credential을 구체적으로 적는다
- generic [ref=f16e172]:
- generic [ref=f16e173]: 규칙 7 본문
- textbox "규칙 7 본문" [ref=f16e174]: SPA의 JavaScript memory를 초기화해도 Keycloak SSO session이 유효하면 다음 authorization request에서 다시 인증 화면을 생략할 수 있다. logout에서는 application session과 authorized client를 각각 어떻게 정리할지 명시한다.
- generic [ref=f16e175]:
- button "위로" [ref=f16e176]
- button "아래로" [disabled] [ref=f16e177]
- button "삭제" [ref=f16e178]
- button "규칙 추가" [ref=f16e179]
- group "적용 조건" [ref=f16e180]:
- generic [ref=f16e182]:
- generic [ref=f16e183]:
- generic [ref=f16e184]: 적용 조건 1
- textbox "적용 조건 1" [ref=f16e185]: 인증 상태를 표나 문서로 정리할 때
- generic [ref=f16e186]:
- button "위로" [disabled] [ref=f16e187]
- button "아래로" [ref=f16e188]
- button "삭제" [ref=f16e189]
- generic [ref=f16e190]:
- generic [ref=f16e191]:
- generic [ref=f16e192]: 적용 조건 2
- textbox "적용 조건 2" [ref=f16e193]: 로그아웃과 만료 동작을 설계할 때
- generic [ref=f16e194]:
- button "위로" [ref=f16e195]
- button "아래로" [ref=f16e196]
- button "삭제" [ref=f16e197]
- generic [ref=f16e198]:
- generic [ref=f16e199]:
- generic [ref=f16e200]: 적용 조건 3
- textbox "적용 조건 3" [ref=f16e201]: 브라우저가 어떤 credential을 저장하거나 전송하는지 설명할 때
- generic [ref=f16e202]:
- button "위로" [ref=f16e203]
- button "아래로" [ref=f16e204]
- button "삭제" [ref=f16e205]
- generic [ref=f16e206]:
- generic [ref=f16e207]:
- generic [ref=f16e208]: 적용 조건 4
- textbox "적용 조건 4" [ref=f16e209]: 여러 구조를 같은 항목으로 비교할 때
- generic [ref=f16e210]:
- button "위로" [ref=f16e211]
- button "아래로" [disabled] [ref=f16e212]
- button "삭제" [ref=f16e213]
- button "적용 조건 추가" [ref=f16e214]
- group "예외" [ref=f16e215]:
- generic [ref=f16e217]:
- generic [ref=f16e218]:
- generic [ref=f16e219]: 예외 1
- textbox "예외 1" [ref=f16e220]: 한 요청 안에서 어느 상태를 말하는지 문맥으로 이미 분명하면 짧은 이름을 쓸 수 있다. 그때도 문서에서 처음 나올 때는 전체 이름을 적어 둔다.
- generic [ref=f16e221]:
- button "위로" [disabled] [ref=f16e222]
- button "아래로" [ref=f16e223]
- button "삭제" [ref=f16e224]
- generic [ref=f16e225]:
- generic [ref=f16e226]:
- generic [ref=f16e227]: 예외 2
- textbox "예외 2" [ref=f16e228]: IdP를 쓰지 않고 애플리케이션이 자체 로그인만 하는 구조에는 SSO session과 access token, refresh token이 없다.
- generic [ref=f16e229]:
- button "위로" [ref=f16e230]
- button "아래로" [disabled] [ref=f16e231]
- button "삭제" [ref=f16e232]
- button "예외 추가" [ref=f16e233]
- group "예시" [ref=f16e234]:
- generic [ref=f16e236]:
- generic [ref=f16e237]:
- generic [ref=f16e238]: 예시 1
- textbox "예시 1" [ref=f16e239]: "IdP SSO session : IdP 도메인의 cookie이고 애플리케이션 memory와 별개다"
- generic [ref=f16e240]:
- button "위로" [disabled] [ref=f16e241]
- button "아래로" [ref=f16e242]
- button "삭제" [ref=f16e243]
- generic [ref=f16e244]:
- generic [ref=f16e245]:
- generic [ref=f16e246]: 예시 2
- textbox "예시 2" [ref=f16e247]: "access token : IdP가 만들고 Resource Server가 서명과 issuer, audience를 검증한다"
- generic [ref=f16e248]:
- button "위로" [ref=f16e249]
- button "아래로" [ref=f16e250]
- button "삭제" [ref=f16e251]
- generic [ref=f16e252]:
- generic [ref=f16e253]:
- generic [ref=f16e254]: 예시 3
- textbox "예시 3" [ref=f16e255]: "refresh token : 새 access token을 받는 장기 credential이다"
- generic [ref=f16e256]:
- button "위로" [ref=f16e257]
- button "아래로" [ref=f16e258]
- button "삭제" [ref=f16e259]
- generic [ref=f16e260]:
- generic [ref=f16e261]:
- generic [ref=f16e262]: 예시 4
- textbox "예시 4" [ref=f16e263]: "애플리케이션 session cookie : server-side 로그인 상태를 찾는 열쇠다"
- generic [ref=f16e264]:
- button "위로" [ref=f16e265]
- button "아래로" [ref=f16e266]
- button "삭제" [ref=f16e267]
- generic [ref=f16e268]:
- generic [ref=f16e269]:
- generic [ref=f16e270]: 예시 5
- textbox "예시 5" [ref=f16e271]: "proxy session cookie : proxy의 auth endpoint에 제시하는 최소 상태다"
- generic [ref=f16e272]:
- button "위로" [ref=f16e273]
- button "아래로" [ref=f16e274]
- button "삭제" [ref=f16e275]
- generic [ref=f16e276]:
- generic [ref=f16e277]:
- generic [ref=f16e278]: 예시 6
- textbox "예시 6" [ref=f16e279]: "CSRF token : cookie가 자동으로 붙는 상태 변경 요청의 의도를 확인한다"
- generic [ref=f16e280]:
- button "위로" [ref=f16e281]
- button "아래로" [ref=f16e282]
- button "삭제" [ref=f16e283]
- generic [ref=f16e284]:
- generic [ref=f16e285]:
- generic [ref=f16e286]: 예시 7
- textbox "예시 7" [ref=f16e287]: "identity header : edge가 확인한 사용자 정보의 투영이고 JWT가 아니다"
- generic [ref=f16e288]:
- button "위로" [ref=f16e289]
- button "아래로" [disabled] [ref=f16e290]
- button "삭제" [ref=f16e291]
- button "예시 추가" [ref=f16e292]
- generic [ref=f16e293]:
- generic [ref=f16e294]: 마지막 검증일
- textbox "마지막 검증일" [ref=f16e295]
- region [ref=f16e296]:
- generic [ref=f16e297]:
- paragraph [ref=f16e298]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f16e299]
- generic [ref=f16e302]:
- generic [ref=f16e303]:
- navigation "문서 경로" [ref=f16e304]:
- link "Reference" [ref=f16e305] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f16e306]: /
- generic [ref=f16e307]: OAuth/OIDC 인증 경계
- generic [ref=f16e308]: /
- link "KeyCloak Patterns" [ref=f16e309] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "OAuth Token과 Application Session을 구분하는 기준" [level=1] [ref=f16e310]
- paragraph [ref=f16e311]: IdP의 SSO session, access token, refresh token, 애플리케이션 session cookie, proxy session cookie는 만든 주체도 소비자도 수명도 다르다. 다섯을 로그인 상태 하나로 부르면 무엇이 만료됐고 무엇을 지워야 하는지 말할 수 없게 된다.
- generic [ref=f16e312]:
- generic [ref=f16e313]:
- term [ref=f16e314]: 유형
- definition [ref=f16e315]: Reference
- generic [ref=f16e316]:
- term [ref=f16e317]: 프로젝트
- definition [ref=f16e318]: KeyCloak Patterns
- generic [ref=f16e319]:
- term [ref=f16e320]: 게시
- definition [ref=f16e321]: 게시 전
- region [ref=f16e322]:
- paragraph [ref=f16e323]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f16e324]
- paragraph [ref=f16e325]: 네 구조를 다 실행해 보면 응답에는 모두 같은 사용자 이름이 나오게 되어서 같은 인증 정보라고 묶기 쉽다.그런데 값이 들어온 곳을 따라가 보면 어떤 때는 JWT 안의 claim이고 어떤 때는 proxy가 만든 헤더다. 둘을 다 로그인 상태라고 부르게 되면 서명을 검증한 것인지 헤더를 확인한 것인지 문장만 봐서는 구분할 수 없게 된다.로그아웃과 만료 처리는 credential마다 다르다. 어떤 상태를 삭제하거나 만료시킬지 정하려면 IdP SSO session, OAuth token, application session을 구분해서 다뤄야 한다.
- article [ref=f16e326]:
- region [ref=f16e327]:
- heading "판단 기준" [level=2] [ref=f16e328]
- list [ref=f16e329]:
- listitem [ref=f16e330]:
- generic [ref=f16e331]: "01"
- generic [ref=f16e332]:
- heading "다섯 상태에 각각 다른 이름을 쓴다" [level=3] [ref=f16e333]
- paragraph [ref=f16e334]: "IdP SSO session, OAuth access token, OAuth refresh token, 애플리케이션 session cookie, proxy session cookie는 서로 다른 것이라서 문서와 코드, 로그에서 같은 이름을 돌려 쓰지 않는다. 로그와 진단 정보에서도 `로그인 상태`라는 표현만 쓰지 않고 실제 session 또는 token 종류를 기록한다."
- listitem [ref=f16e335]:
- generic [ref=f16e336]: "02"
- generic [ref=f16e337]:
- heading "만든 주체와 주된 소비자로 구분한다" [level=3] [ref=f16e338]
- paragraph [ref=f16e339]: access token은 IdP가 만들고 Resource Server가 소비하게 되고, 애플리케이션 session cookie는 애플리케이션이 만들어 자기 로그인 상태를 찾는 데 쓰게 되며, proxy session cookie는 proxy의 auth endpoint에만 제시된다. 화면에 같은 사용자 이름이 보이더라도 credential을 발급한 주체와 검증하는 주체가 다르면 별도의 상태로 다룬다.
- listitem [ref=f16e340]:
- generic [ref=f16e341]: "03"
- generic [ref=f16e342]:
- heading "cookie가 token을 담고 있다고 쓰지 않는다" [level=3] [ref=f16e343]
- paragraph [ref=f16e344]: 애플리케이션 session cookie는 server-side 상태를 찾는 열쇠다. 실제 access token과 refresh token은 별도 store에 있어서 cookie 안에는 없다. proxy session cookie는 같은 모델이 아니다. 서버에 상태를 두지 않고 최소 정보를 cookie 자체에 담아 proxy가 검증하는 구성일 수 있다. 두 cookie를 같은 문장으로 설명하지 않는다. cookie를 token map의 직렬화라고 설명하게 되면 구현 설명이 틀리게 되고, 그 store를 어디에 둘지가 별도 문제라는 것도 함께 가려지게 된다.
- listitem [ref=f16e345]:
- generic [ref=f16e346]: "04"
- generic [ref=f16e347]:
- heading "브라우저에 없다는 말의 대상을 밝힌다" [level=3] [ref=f16e348]
- paragraph [ref=f16e349]: 브라우저 JavaScript에 OAuth token을 전달하지 않는 구조에서도 인증 상태는 존재한다. BFF의 HttpOnly session cookie나 IdP 도메인의 SSO cookie는 각각 별도로 유지될 수 있다. 무엇이 없는지를 적지 않으면 브라우저에 인증 상태가 아예 없다는 뜻으로 읽힌다.
- listitem [ref=f16e350]:
- generic [ref=f16e351]: "05"
- generic [ref=f16e352]:
- heading "영구 저장소에 없는 것과 실행 중에 없는 것을 나눈다" [level=3] [ref=f16e353]
- paragraph [ref=f16e354]: OAuth token을 JavaScript memory에만 보관하면 Web Storage에 지속적으로 저장하지는 않는다. 실행 중 같은 origin의 script가 응답이나 지역 변수에 접근하는 문제는 별도다. 두 문장을 같은 증거로 쓰게 되면 XSS 위험이 줄었다는 잘못된 결론이 나오게 된다.
- listitem [ref=f16e355]:
- generic [ref=f16e356]: "06"
- generic [ref=f16e357]:
- heading "로그아웃 범위를 상태별로 적는다" [level=3] [ref=f16e358]
- paragraph [ref=f16e359]: 애플리케이션 상태를 지우는 것과 IdP session을 끝내는 것은 다르고, 이미 발급된 self-contained JWT는 만료 전까지 API에서 계속 통하게 된다. self-contained JWT를 stateless하게 검증하면서 denylist나 introspection을 사용하지 않는 구성에서는 애플리케이션 logout만으로 이미 발급된 access token을 즉시 무효화할 수 없다. 이 경우 짧은 access token TTL을 사용해 유효 시간을 제한한다.
- listitem [ref=f16e360]:
- generic [ref=f16e361]: "07"
- generic [ref=f16e362]:
- heading "Logout 대상 credential을 구체적으로 적는다" [level=3] [ref=f16e363]
- paragraph [ref=f16e364]: SPA의 JavaScript memory를 초기화해도 Keycloak SSO session이 유효하면 다음 authorization request에서 다시 인증 화면을 생략할 수 있다. logout에서는 application session과 authorized client를 각각 어떻게 정리할지 명시한다.
- region [ref=f16e365]:
- heading "적용할 때" [level=2] [ref=f16e366]
- list [ref=f16e367]:
- listitem [ref=f16e368]: 인증 상태를 표나 문서로 정리할 때
- listitem [ref=f16e369]: 로그아웃과 만료 동작을 설계할 때
- listitem [ref=f16e370]: 브라우저가 어떤 credential을 저장하거나 전송하는지 설명할 때
- listitem [ref=f16e371]: 여러 구조를 같은 항목으로 비교할 때
- region [ref=f16e372]:
- heading "예외와 주의" [level=2] [ref=f16e373]
- list [ref=f16e374]:
- listitem [ref=f16e375]: 한 요청 안에서 어느 상태를 말하는지 문맥으로 이미 분명하면 짧은 이름을 쓸 수 있다. 그때도 문서에서 처음 나올 때는 전체 이름을 적어 둔다.
- listitem [ref=f16e376]: IdP를 쓰지 않고 애플리케이션이 자체 로그인만 하는 구조에는 SSO session과 access token, refresh token이 없다.
- region [ref=f16e377]:
- heading "예시" [level=2] [ref=f16e378]
- list [ref=f16e379]:
- listitem [ref=f16e380]: "IdP SSO session : IdP 도메인의 cookie이고 애플리케이션 memory와 별개다"
- listitem [ref=f16e381]: "access token : IdP가 만들고 Resource Server가 서명과 issuer, audience를 검증한다"
- listitem [ref=f16e382]: "refresh token : 새 access token을 받는 장기 credential이다"
- listitem [ref=f16e383]: "애플리케이션 session cookie : server-side 로그인 상태를 찾는 열쇠다"
- listitem [ref=f16e384]: "proxy session cookie : proxy의 auth endpoint에 제시하는 최소 상태다"
- listitem [ref=f16e385]: "CSRF token : cookie가 자동으로 붙는 상태 변경 요청의 의도를 확인한다"
- listitem [ref=f16e386]: "identity header : edge가 확인한 사용자 정보의 투영이고 JWT가 아니다"
- paragraph [ref=f16e387]: 마지막 검증
- region [ref=f16e388]:
- paragraph [ref=f16e389]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f16e390]
- list [ref=f16e391]:
- listitem [ref=f16e392]:
- link "JavaScript memory의 OAuth token과 Keycloak SSO session을 구분한 Case다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f16e393] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- generic [ref=f16e394]: JavaScript memory의 OAuth token과 Keycloak SSO session을 구분한 Case다.
- strong [ref=f16e395]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
- generic [ref=f16e396]:
- listitem [ref=f16e397]:
- link "같은 요청 안에서 session cookie와 access token이 함께 움직인다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f16e398] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f16e399]: 같은 요청 안에서 session cookie와 access token이 함께 움직인다.
- strong [ref=f16e400]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f16e401]:
- listitem [ref=f16e402]:
- link "BFF에서는 session cookie, JavaScript가 읽는 CSRF token, server-side OAuth token을 각각 다른 용도로 사용한다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f16e403] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f16e404]: BFF에서는 session cookie, JavaScript가 읽는 CSRF token, server-side OAuth token을 각각 다른 용도로 사용한다.
- strong [ref=f16e405]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f16e406]:
- listitem [ref=f16e407]:
- link "Forward-Auth에서는 upstream이 JWT를 직접 검증하지 않고 proxy session을 기반으로 edge가 만든 identity header를 사용한다. Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f16e408] [cursor=pointer]:
- /url: /cases/identity-header-trust
- generic [ref=f16e409]: Forward-Auth에서는 upstream이 JWT를 직접 검증하지 않고 proxy session을 기반으로 edge가 만든 identity header를 사용한다.
- strong [ref=f16e410]: Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유
- generic [ref=f16e411]:
- complementary [ref=f16e412]:
- heading "작업 상태" [level=2] [ref=f16e413]
- status "편집 상태" [ref=f16e414]: 저장됨
- generic [ref=f16e415]:
- generic [ref=f16e416]:
- term [ref=f16e417]: 저장 버전
- definition [ref=f16e418]: "12"
- generic [ref=f16e419]:
- term [ref=f16e420]: 종류
- definition [ref=f16e421]: REFERENCE
- paragraph [ref=f16e422]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f16e423]:
- button "저장" [disabled] [ref=f16e424]
- button "게시" [ref=f16e425]
- paragraph [ref=f16e426]: 버전 12으로 저장했습니다.
@@ -0,0 +1,455 @@
- generic [ref=f17e3]:
- link "본문으로 건너뛰기" [ref=f17e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f17e5]:
- generic [ref=f17e6]:
- link "TechLog Studio" [ref=f17e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f17e8]: Studio
- navigation "Studio 주 탐색" [ref=f17e10]:
- link "작업본" [ref=f17e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f17e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f17e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f17e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f17e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f17e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f17e17]
- main [ref=f17e18]:
- generic [ref=f17e19]:
- generic [ref=f17e20]:
- region [ref=f17e21]:
- generic [ref=f17e22]:
- paragraph [ref=f17e23]: REFERENCE · VERSION 33
- heading "문서 편집" [level=1] [ref=f17e24]
- paragraph [ref=f17e25]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
- region [ref=f17e26]:
- generic [ref=f17e27]:
- paragraph [ref=f17e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f17e29]
- generic [ref=f17e30]:
- generic [ref=f17e31]:
- generic [ref=f17e32]: 제목
- textbox "제목" [ref=f17e33]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
- generic [ref=f17e34]:
- generic [ref=f17e35]: slug
- textbox "slug" [ref=f17e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: authorization-code-endpoint-credential-movement
- generic [ref=f17e37]:
- generic [ref=f17e38]: 요약
- textbox "요약" [ref=f17e39]: "Authorization Code Flow에서 브라우저와 client, Authorization Server, Resource Server가 주고받는 값을 endpoint별로 정리한다. 특히 `client_secret`과 authorization code, access token이 어느 요청에 포함되는지를 구분한다."
- generic [ref=f17e40]:
- generic [ref=f17e41]: Topic
- combobox "Topic" [ref=f17e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f17e43]:
- generic [ref=f17e44]: Project
- combobox "Project" [ref=f17e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f17e46]:
- generic [ref=f17e48]:
- generic [ref=f17e49]:
- generic [ref=f17e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f17e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준" [disabled]
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
- generic [ref=f17e52]:
- generic [ref=f17e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f17e54]: 브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다.
- generic [ref=f17e55]:
- button "위로" [disabled] [ref=f17e56]
- button "아래로" [ref=f17e57]
- button "삭제" [ref=f17e58]
- generic [ref=f17e59]:
- generic [ref=f17e60]:
- generic [ref=f17e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f17e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준" [disabled]
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f17e63]:
- generic [ref=f17e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f17e65]: confidential client가 token endpoint에서 client 인증을 수행하는 흐름을 보여 준다.
- generic [ref=f17e66]:
- button "위로" [ref=f17e67]
- button "아래로" [ref=f17e68]
- button "삭제" [ref=f17e69]
- generic [ref=f17e70]:
- generic [ref=f17e71]:
- generic [ref=f17e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f17e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
- option "외부 IdP Federation과 Application 인증 경계"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준"
- option "Public Client와 Confidential Client 구분 기준" [selected]
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
- generic [ref=f17e74]:
- generic [ref=f17e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f17e76]: public/confidential client 구분에 따라 token endpoint의 client 인증 방식이 달라지고, Authorization Code Flow에서는 PKCE 적용 여부도 함께 결정한다.
- generic [ref=f17e77]:
- button "위로" [ref=f17e78]
- button "아래로" [disabled] [ref=f17e79]
- button "삭제" [ref=f17e80]
- button "관계 추가" [ref=f17e81]
- region [ref=f17e82]:
- generic [ref=f17e83]:
- paragraph [ref=f17e84]: REFERENCE
- heading "재사용할 기준" [level=2] [ref=f17e85]
- generic [ref=f17e86]:
- generic [ref=f17e87]: 목적
- textbox "목적" [ref=f17e88]: "Authorization Endpoint와 Token Endpoint는 역할과 호출 방식이 다르다. 이 구분을 해야 SPA에서 client_secret이 어디로 갔는지, PKCE가 어느 구간을 지키는지 이해하기 쉽다. 하나는 브라우저의 full-page navigation이고 하나는 server-to-server 호출이 될 수도 있고 browser-to-server 호출이 될 수도 있다. 노출되는 것도, 인증하는 방법도 다르다. Authorization Endpoint 경로 : 브라우저 주소창 남는 곳 : 히스토리·서버 로그·referrer client 인증 : x Token Endpoint 경로 : body와 Authorization 헤더 보내는 쪽 : client 종류에 따라 server 또는 브라우저 client 인증 : o"
- group "규칙" [ref=f17e89]:
- generic [ref=f17e91]:
- generic [ref=f17e92]:
- generic [ref=f17e93]: 규칙 1 제목
- textbox "규칙 1 제목" [ref=f17e94]: Authorization Endpoint에는 client_secret을 보내지 않는다
- generic [ref=f17e95]:
- generic [ref=f17e96]: 규칙 1 본문
- textbox "규칙 1 본문" [ref=f17e97]: Authorization request는 브라우저 navigation으로 전송되므로 URL이 주소창과 브라우저 히스토리, Authorization Server 접근 로그에 기록될 수 있고 이후 navigation에서는 Referrer-Policy 설정에 따라 referrer에도 포함될 수 있다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. secret이 필요한 인증은 아직 하지 않는다. 따라서 authorization request URL에는 노출돼도 되는 값만 포함한다.
- generic [ref=f17e98]:
- button "위로" [disabled] [ref=f17e99]
- button "아래로" [ref=f17e100]
- button "삭제" [ref=f17e101]
- generic [ref=f17e102]:
- generic [ref=f17e103]:
- generic [ref=f17e104]: 규칙 2 제목
- textbox "규칙 2 제목" [ref=f17e105]: Token Endpoint에서 비로소 client를 인증한다
- generic [ref=f17e106]:
- generic [ref=f17e107]: 규칙 2 본문
- textbox "규칙 2 본문" [ref=f17e108]: "token request는 authorization code와 `redirect_uri`, `code_verifier` 등을 request body로 보내고, confidential client는 `client_secret_basic` 같은 방식으로 token endpoint에서 client 인증도 수행한다. 주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 debug 로그, reverse proxy 로그, tracing과 APM, packet capture에 남을 수 있어서 credential masking을 따로 둔다. 이 요청을 누가 보내는지는 client 종류에 따라 갈린다. server가 보내면 server-to-server이고, secret이 없는 SPA가 보내면 브라우저가 직접 보낸다. token endpoint를 server 안에서만 부르게 하려면 client 종류부터 confidential로 정해야 한다."
- generic [ref=f17e109]:
- button "위로" [ref=f17e110]
- button "아래로" [ref=f17e111]
- button "삭제" [ref=f17e112]
- generic [ref=f17e113]:
- generic [ref=f17e114]:
- generic [ref=f17e115]: 규칙 3 제목
- textbox "규칙 3 제목" [ref=f17e116]: PKCE는 두 요청을 같은 주체에 묶는다
- generic [ref=f17e117]:
- generic [ref=f17e118]: 규칙 3 본문
- textbox "규칙 3 본문" [ref=f17e119]: 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서 이 두개가 일치하는지 확인 한다. 이 2개가 일치해야 토큰 교환이 되게 된다. code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
- generic [ref=f17e120]:
- button "위로" [ref=f17e121]
- button "아래로" [ref=f17e122]
- button "삭제" [ref=f17e123]
- generic [ref=f17e124]:
- generic [ref=f17e125]:
- generic [ref=f17e126]: 규칙 4 제목
- textbox "규칙 4 제목" [ref=f17e127]: issuer 검증값과 JWK 조회 주소를 같은 값으로 맞추려 하지 않는다
- generic [ref=f17e128]:
- generic [ref=f17e129]: 규칙 4 본문
- textbox "규칙 4 본문" [ref=f17e130]: issuer는 요청을 보내는 주소가 아니라 token의 canonical issuer identifier다. 검증은 발급된 token의 iss claim이 그 값과 같은지를 본다. JWK 조회 주소는 실제로 공개키를 가져오는 network 경로다. 이 예제에서는 브라우저가 보는 주소와 컨테이너 안에서 닿는 주소가 다르다. 컨테이너 안에서는 자기 localhost가 그 서버가 아니므로 service 이름을 써야 하고, 브라우저는 그 이름에 닿지 못한다. issuer 검증값과 endpoint 연결 주소는 따로 구성한다. 둘을 하나로 맞추려 하면 로그인 redirect가 깨지거나 서버가 키를 못 가져온다.
- generic [ref=f17e131]:
- button "위로" [ref=f17e132]
- button "아래로" [ref=f17e133]
- button "삭제" [ref=f17e134]
- generic [ref=f17e135]:
- generic [ref=f17e136]:
- generic [ref=f17e137]: 규칙 5 제목
- textbox "규칙 5 제목" [ref=f17e138]: Resource API는 서명만 보고 끝내지 않는다
- generic [ref=f17e139]:
- generic [ref=f17e140]: 규칙 5 본문
- textbox "규칙 5 본문" [ref=f17e141]: 서명이 맞다는 것은 그 IdP가 발급했다는 뜻일 뿐이다. 같은 IdP가 다른 API용으로 발급한 token도 서명은 맞다. Resource Server는 서명과 함께 issuer, 유효 시간, audience를 검증한다. 특히 audience를 검증해야 다른 resource를 대상으로 발급된 token을 현재 API에서 받아들이지 않는다.
- generic [ref=f17e142]:
- button "위로" [ref=f17e143]
- button "아래로" [ref=f17e144]
- button "삭제" [ref=f17e145]
- generic [ref=f17e146]:
- generic [ref=f17e147]:
- generic [ref=f17e148]: 규칙 6 제목
- textbox "규칙 6 제목" [ref=f17e149]: redirect_uri는 exact match로 좁힌다
- generic [ref=f17e150]:
- generic [ref=f17e151]: 규칙 6 본문
- textbox "규칙 6 본문" [ref=f17e152]: wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른 경로로도 code가 갈 수 있다. 실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다. 등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
- generic [ref=f17e153]:
- button "위로" [ref=f17e154]
- button "아래로" [ref=f17e155]
- button "삭제" [ref=f17e156]
- generic [ref=f17e157]:
- generic [ref=f17e158]:
- generic [ref=f17e159]: 규칙 7 제목
- textbox "규칙 7 제목" [ref=f17e160]: 로그인 구간과 API 호출 구간을 한 줄로 그리지 않는다
- generic [ref=f17e161]:
- generic [ref=f17e162]: 규칙 7 본문
- textbox "규칙 7 본문" [ref=f17e163]: 로그인 구간은 authorization request에서 시작해 callback과 code 교환을 지나 로그인 상태를 만드는 데까지다. API 호출 구간은 브라우저 입력, 중간 계층의 credential 변환, 보호 자원의 검증, 최종 응답이다. 로그인 구간과 애플리케이션 API 호출 구간은 호출 주체가 다를 수 있으므로 별도로 그린다. 그래야 code 교환 주체와 Resource Server 호출 주체를 각각 확인할 수 있다.
- generic [ref=f17e164]:
- button "위로" [ref=f17e165]
- button "아래로" [disabled] [ref=f17e166]
- button "삭제" [ref=f17e167]
- button "규칙 추가" [ref=f17e168]
- group "적용 조건" [ref=f17e169]:
- generic [ref=f17e171]:
- generic [ref=f17e172]:
- generic [ref=f17e173]: 적용 조건 1
- textbox "적용 조건 1" [ref=f17e174]: Authorization Code Flow를 쓰는 client를 설정하거나 문서로 설명할 때
- generic [ref=f17e175]:
- button "위로" [disabled] [ref=f17e176]
- button "아래로" [ref=f17e177]
- button "삭제" [ref=f17e178]
- generic [ref=f17e179]:
- generic [ref=f17e180]:
- generic [ref=f17e181]: 적용 조건 2
- textbox "적용 조건 2" [ref=f17e182]: 브라우저 요청과 server-to-server 요청이 한 흐름에 섞여 있을 때
- generic [ref=f17e183]:
- button "위로" [ref=f17e184]
- button "아래로" [ref=f17e185]
- button "삭제" [ref=f17e186]
- generic [ref=f17e187]:
- generic [ref=f17e188]:
- generic [ref=f17e189]: 적용 조건 3
- textbox "적용 조건 3" [ref=f17e190]: endpoint별로 무엇이 노출되는지 나눠야 할 때
- generic [ref=f17e191]:
- button "위로" [ref=f17e192]
- button "아래로" [ref=f17e193]
- button "삭제" [ref=f17e194]
- generic [ref=f17e195]:
- generic [ref=f17e196]:
- generic [ref=f17e197]: 적용 조건 4
- textbox "적용 조건 4" [ref=f17e198]: PKCE 적용과 client 인증 방식을 정할 때
- generic [ref=f17e199]:
- button "위로" [ref=f17e200]
- button "아래로" [disabled] [ref=f17e201]
- button "삭제" [ref=f17e202]
- button "적용 조건 추가" [ref=f17e203]
- group "예외" [ref=f17e204]:
- generic [ref=f17e206]:
- generic [ref=f17e207]:
- generic [ref=f17e208]: 예외 1
- textbox "예외 1" [ref=f17e209]: Client Credentials처럼 사용자 없이 token을 받는 흐름은 Authorization Endpoint를 지나지 않는다.
- generic [ref=f17e210]:
- button "위로" [disabled] [ref=f17e211]
- button "아래로" [ref=f17e212]
- button "삭제" [ref=f17e213]
- generic [ref=f17e214]:
- generic [ref=f17e215]:
- generic [ref=f17e216]: 예외 2
- textbox "예외 2" [ref=f17e217]: "Device Authorization Grant는 브라우저 redirect가 아니라 device code와 user code를 사용하므로 이 문서의 `redirect_uri` 흐름과는 별도로 본다."
- generic [ref=f17e218]:
- button "위로" [ref=f17e219]
- button "아래로" [disabled] [ref=f17e220]
- button "삭제" [ref=f17e221]
- button "예외 추가" [ref=f17e222]
- group "예시" [ref=f17e223]:
- generic [ref=f17e225]:
- generic [ref=f17e226]:
- generic [ref=f17e227]: 예시 1
- textbox "예시 1" [ref=f17e228]: authorization request에는 code_challenge_method=S256이 있고 client secret은 없다
- generic [ref=f17e229]:
- button "위로" [disabled] [ref=f17e230]
- button "아래로" [ref=f17e231]
- button "삭제" [ref=f17e232]
- generic [ref=f17e233]:
- generic [ref=f17e234]:
- generic [ref=f17e235]: 예시 2
- textbox "예시 2" [ref=f17e236]: token request에는 code_verifier가 있다. secret을 가진 client는 이 요청에서 자기를 인증한다
- generic [ref=f17e237]:
- button "위로" [ref=f17e238]
- button "아래로" [ref=f17e239]
- button "삭제" [ref=f17e240]
- generic [ref=f17e241]:
- generic [ref=f17e242]:
- generic [ref=f17e243]: 예시 3
- textbox "예시 3" [ref=f17e244]: expected issuer는 http://localhost:8080/realms/keycloak-patterns 이고 JWK 조회는 컨테이너 network 주소를 쓴다
- generic [ref=f17e245]:
- button "위로" [ref=f17e246]
- button "아래로" [ref=f17e247]
- button "삭제" [ref=f17e248]
- generic [ref=f17e249]:
- generic [ref=f17e250]:
- generic [ref=f17e251]: 예시 4
- textbox "예시 4" [ref=f17e252]: audience에 keycloak-pattern-api 가 없으면 invalid_token 결과가 되어 401이 된다
- generic [ref=f17e253]:
- button "위로" [ref=f17e254]
- button "아래로" [ref=f17e255]
- button "삭제" [ref=f17e256]
- generic [ref=f17e257]:
- generic [ref=f17e258]:
- generic [ref=f17e259]: 예시 5
- textbox "예시 5" [ref=f17e260]: redirect allowlist에 wildcard가 있으면 등록한 host의 다른 경로로도 code가 갈 수 있다
- generic [ref=f17e261]:
- button "위로" [ref=f17e262]
- button "아래로" [disabled] [ref=f17e263]
- button "삭제" [ref=f17e264]
- button "예시 추가" [ref=f17e265]
- generic [ref=f17e266]:
- generic [ref=f17e267]: 마지막 검증일
- textbox "마지막 검증일" [ref=f17e268]: 2026-08-25
- region [ref=f17e269]:
- generic [ref=f17e270]:
- paragraph [ref=f17e271]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f17e272]
- generic [ref=f17e275]:
- generic [ref=f17e276]:
- navigation "문서 경로" [ref=f17e277]:
- link "Reference" [ref=f17e278] [cursor=pointer]:
- /url: /explore/references
- generic [ref=f17e279]: /
- generic [ref=f17e280]: OAuth/OIDC 인증 경계
- generic [ref=f17e281]: /
- link "KeyCloak Patterns" [ref=f17e282] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [level=1] [ref=f17e283]
- paragraph [ref=f17e284]: "Authorization Code Flow에서 브라우저와 client, Authorization Server, Resource Server가 주고받는 값을 endpoint별로 정리한다. 특히 `client_secret`과 authorization code, access token이 어느 요청에 포함되는지를 구분한다."
- generic [ref=f17e285]:
- generic [ref=f17e286]:
- term [ref=f17e287]: 유형
- definition [ref=f17e288]: Reference
- generic [ref=f17e289]:
- term [ref=f17e290]: 프로젝트
- definition [ref=f17e291]: KeyCloak Patterns
- generic [ref=f17e292]:
- term [ref=f17e293]: 게시
- definition [ref=f17e294]: 2026.08.25
- region [ref=f17e295]:
- paragraph [ref=f17e296]: Purpose
- heading "이 기준을 쓰는 이유" [level=2] [ref=f17e297]
- paragraph [ref=f17e298]: "Authorization Endpoint와 Token Endpoint는 역할과 호출 방식이 다르다.이 구분을 해야 SPA에서 client_secret이 어디로 갔는지, PKCE가 어느 구간을 지키는지 이해하기 쉽다.하나는 브라우저의 full-page navigation이고 하나는 server-to-server 호출이 될 수도 있고 browser-to-server 호출이 될 수도 있다.노출되는 것도, 인증하는 방법도 다르다.Authorization Endpoint경로 : 브라우저 주소창 남는 곳 : 히스토리·서버 로그·referrer client 인증 : xToken Endpoint경로 : body와 Authorization 헤더 보내는 쪽 : client 종류에 따라 server 또는 브라우저 client 인증 : o"
- article [ref=f17e299]:
- region [ref=f17e300]:
- heading "판단 기준" [level=2] [ref=f17e301]
- list [ref=f17e302]:
- listitem [ref=f17e303]:
- generic [ref=f17e304]: "01"
- generic [ref=f17e305]:
- heading "Authorization Endpoint에는 client_secret을 보내지 않는다" [level=3] [ref=f17e306]
- paragraph [ref=f17e307]: Authorization request는 브라우저 navigation으로 전송되므로 URL이 주소창과 브라우저 히스토리, Authorization Server 접근 로그에 기록될 수 있고 이후 navigation에서는 Referrer-Policy 설정에 따라 referrer에도 포함될 수 있다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. secret이 필요한 인증은 아직 하지 않는다. 따라서 authorization request URL에는 노출돼도 되는 값만 포함한다.
- listitem [ref=f17e308]:
- generic [ref=f17e309]: "02"
- generic [ref=f17e310]:
- heading "Token Endpoint에서 비로소 client를 인증한다" [level=3] [ref=f17e311]
- paragraph [ref=f17e312]: "token request는 authorization code와 `redirect_uri`, `code_verifier` 등을 request body로 보내고, confidential client는 `client_secret_basic` 같은 방식으로 token endpoint에서 client 인증도 수행한다. 주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 debug 로그, reverse proxy 로그, tracing과 APM, packet capture에 남을 수 있어서 credential masking을 따로 둔다. 이 요청을 누가 보내는지는 client 종류에 따라 갈린다. server가 보내면 server-to-server이고, secret이 없는 SPA가 보내면 브라우저가 직접 보낸다. token endpoint를 server 안에서만 부르게 하려면 client 종류부터 confidential로 정해야 한다."
- listitem [ref=f17e313]:
- generic [ref=f17e314]: "03"
- generic [ref=f17e315]:
- heading "PKCE는 두 요청을 같은 주체에 묶는다" [level=3] [ref=f17e316]
- paragraph [ref=f17e317]: 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서 이 두개가 일치하는지 확인 한다. 이 2개가 일치해야 토큰 교환이 되게 된다. code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
- listitem [ref=f17e318]:
- generic [ref=f17e319]: "04"
- generic [ref=f17e320]:
- heading "issuer 검증값과 JWK 조회 주소를 같은 값으로 맞추려 하지 않는다" [level=3] [ref=f17e321]
- paragraph [ref=f17e322]: issuer는 요청을 보내는 주소가 아니라 token의 canonical issuer identifier다. 검증은 발급된 token의 iss claim이 그 값과 같은지를 본다. JWK 조회 주소는 실제로 공개키를 가져오는 network 경로다. 이 예제에서는 브라우저가 보는 주소와 컨테이너 안에서 닿는 주소가 다르다. 컨테이너 안에서는 자기 localhost가 그 서버가 아니므로 service 이름을 써야 하고, 브라우저는 그 이름에 닿지 못한다. issuer 검증값과 endpoint 연결 주소는 따로 구성한다. 둘을 하나로 맞추려 하면 로그인 redirect가 깨지거나 서버가 키를 못 가져온다.
- listitem [ref=f17e323]:
- generic [ref=f17e324]: "05"
- generic [ref=f17e325]:
- heading "Resource API는 서명만 보고 끝내지 않는다" [level=3] [ref=f17e326]
- paragraph [ref=f17e327]: 서명이 맞다는 것은 그 IdP가 발급했다는 뜻일 뿐이다. 같은 IdP가 다른 API용으로 발급한 token도 서명은 맞다. Resource Server는 서명과 함께 issuer, 유효 시간, audience를 검증한다. 특히 audience를 검증해야 다른 resource를 대상으로 발급된 token을 현재 API에서 받아들이지 않는다.
- listitem [ref=f17e328]:
- generic [ref=f17e329]: "06"
- generic [ref=f17e330]:
- heading "redirect_uri는 exact match로 좁힌다" [level=3] [ref=f17e331]
- paragraph [ref=f17e332]: wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른 경로로도 code가 갈 수 있다. 실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다. 등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
- listitem [ref=f17e333]:
- generic [ref=f17e334]: "07"
- generic [ref=f17e335]:
- heading "로그인 구간과 API 호출 구간을 한 줄로 그리지 않는다" [level=3] [ref=f17e336]
- paragraph [ref=f17e337]: 로그인 구간은 authorization request에서 시작해 callback과 code 교환을 지나 로그인 상태를 만드는 데까지다. API 호출 구간은 브라우저 입력, 중간 계층의 credential 변환, 보호 자원의 검증, 최종 응답이다. 로그인 구간과 애플리케이션 API 호출 구간은 호출 주체가 다를 수 있으므로 별도로 그린다. 그래야 code 교환 주체와 Resource Server 호출 주체를 각각 확인할 수 있다.
- region [ref=f17e338]:
- heading "적용할 때" [level=2] [ref=f17e339]
- list [ref=f17e340]:
- listitem [ref=f17e341]: Authorization Code Flow를 쓰는 client를 설정하거나 문서로 설명할 때
- listitem [ref=f17e342]: 브라우저 요청과 server-to-server 요청이 한 흐름에 섞여 있을 때
- listitem [ref=f17e343]: endpoint별로 무엇이 노출되는지 나눠야 할 때
- listitem [ref=f17e344]: PKCE 적용과 client 인증 방식을 정할 때
- region [ref=f17e345]:
- heading "예외와 주의" [level=2] [ref=f17e346]
- list [ref=f17e347]:
- listitem [ref=f17e348]: Client Credentials처럼 사용자 없이 token을 받는 흐름은 Authorization Endpoint를 지나지 않는다.
- listitem [ref=f17e349]: "Device Authorization Grant는 브라우저 redirect가 아니라 device code와 user code를 사용하므로 이 문서의 `redirect_uri` 흐름과는 별도로 본다."
- region [ref=f17e350]:
- heading "예시" [level=2] [ref=f17e351]
- list [ref=f17e352]:
- listitem [ref=f17e353]: authorization request에는 code_challenge_method=S256이 있고 client secret은 없다
- listitem [ref=f17e354]: token request에는 code_verifier가 있다. secret을 가진 client는 이 요청에서 자기를 인증한다
- listitem [ref=f17e355]: expected issuer는 http://localhost:8080/realms/keycloak-patterns 이고 JWK 조회는 컨테이너 network 주소를 쓴다
- listitem [ref=f17e356]: audience에 keycloak-pattern-api 가 없으면 invalid_token 결과가 되어 401이 된다
- listitem [ref=f17e357]: redirect allowlist에 wildcard가 있으면 등록한 host의 다른 경로로도 code가 갈 수 있다
- paragraph [ref=f17e358]: 마지막 검증 2026.08.25
- region [ref=f17e359]:
- paragraph [ref=f17e360]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f17e361]
- list [ref=f17e362]:
- listitem [ref=f17e363]:
- link "브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f17e364] [cursor=pointer]:
- /url: /cases/spa-browser-credential-boundary
- generic [ref=f17e365]: 브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다.
- strong [ref=f17e366]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
- generic [ref=f17e367]:
- listitem [ref=f17e368]:
- link "confidential client가 token endpoint에서 client 인증을 수행하는 흐름을 보여 준다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f17e369] [cursor=pointer]:
- /url: /cases/split-custody-access-token
- generic [ref=f17e370]: confidential client가 token endpoint에서 client 인증을 수행하는 흐름을 보여 준다.
- strong [ref=f17e371]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
- generic [ref=f17e372]:
- complementary [ref=f17e373]:
- heading "작업 상태" [level=2] [ref=f17e374]
- status "편집 상태" [ref=f17e375]: 저장됨
- generic [ref=f17e376]:
- generic [ref=f17e377]:
- term [ref=f17e378]: 저장 버전
- definition [ref=f17e379]: "33"
- generic [ref=f17e380]:
- term [ref=f17e381]: 종류
- definition [ref=f17e382]: REFERENCE
- paragraph [ref=f17e383]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f17e384]:
- button "저장" [disabled] [ref=f17e385]
- button "게시" [ref=f17e386]
- paragraph [ref=f17e387]: 버전 33으로 저장했습니다.

Some files were not shown because too many files have changed in this diff Show More