refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -0,0 +1,338 @@
{
"schemaVersion": 4,
"runId": "2026-09-19-1926-remediation-reference-pattern-selection",
"project": "keycloak",
"record": "docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"startedAt": "2026-09-19T10:26:47+00:00",
"finishedAt": "2026-09-19T10:26:49+00:00",
"stages": [
{
"id": "S1",
"name": "코드베이스 → SSOT",
"skill": "analyzing-codebase-for-tech-log",
"runBy": "ssot-analyst",
"status": "SKIPPED",
"skipReason": "기존 SSOT docs/keycloak/final/document.md가 있고 이번 리뷰는 이미 반영된 2026-09-19 Record 결과의 current remediation 원장 생성이다. SSOT 본문을 다시 수정하지 않는다.",
"skillEcho": "",
"skillRevision": "edd45dfec66d155a621cd41186b83b5582f2244c",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:47+00:00",
"elapsedSeconds": 0,
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S2",
"name": "SSOT → 분해 계약",
"skill": "deriving-tech-log-root-tree",
"runBy": "tree-deriver",
"status": "SKIPPED",
"skipReason": "해당 기록은 tech-log-tree.json의 기존 PROMOTE/CONFIRMED 노드이며 Tree/분해 계약이 이미 PASS다. 이번 remediation에서는 분해 계약을 변경하지 않는다.",
"skillEcho": "",
"skillRevision": "ab59130196d79e947b32e3b5e6b75335a9e5c1eb",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:47+00:00",
"elapsedSeconds": 0,
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S3",
"name": "글감 → 기록",
"skill": "writing-tech-log-records",
"runBy": "record-writer",
"status": "DONE",
"skipReason": "",
"skillEcho": "**인용한 줄은 SSOT 에서 찾아 대조한다.**",
"skillRevision": null,
"inputs": [],
"outputs": [
"docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md"
],
"gates": [
{
"cmd": "python3 scripts/studio-body.py docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md -o runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S3/studio-body.md",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:47+00:00"
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S3/studio-body.md --frontend /shared/codebase/tech-log-frontend",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs keycloak",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
}
],
"notes": "현재 remediation에서 record-writer 역할 계약으로 최종 Record를 다시 읽고 S3 gate를 실행했다. 본문은 추가 수정하지 않았다. live source repo는 현재 머신에 없어 repo reconciliation은 별도 fact review에서 UNVERIFIABLE로 기록한다.",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:48+00:00",
"elapsedSeconds": 1,
"generation": 1,
"owner": "chatgpt-current-remediation",
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S4",
"name": "기록 → 그림",
"skill": "technical-visualizer",
"runBy": "diagram-maker",
"status": "SKIPPED",
"skipReason": "이 기록은 Reference이고 이번 수정은 선택 기준 문장 정리다. 새 순서·구조·측정 관계가 추가되지 않았으며 Reference는 TechLog 그림 렌더 대상이 아니므로 새 SVG를 만들지 않는다.",
"skillEcho": "",
"skillRevision": "edd45dfec66d155a621cd41186b83b5582f2244c",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:48+00:00",
"elapsedSeconds": 0,
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S5",
"name": "AI 티 제거",
"skill": "rewriting-technical-prose-naturally",
"runBy": "prose-rewriter",
"status": "DONE",
"skipReason": "",
"skillEcho": "This is an **editorial** pass. The source's facts, evidence, causal chain, uncertainty, decision status, and technical depth are the contract.",
"skillRevision": null,
"inputs": [],
"outputs": [
"docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md"
],
"gates": [
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/style_profile.mjs docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 1,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S5/studio-body.md --frontend /shared/codebase/tech-log-frontend",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs keycloak",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:48+00:00"
}
],
"notes": "현재 최종 prose를 prose-rewriter 계약으로 다시 읽고 검사했다. hard prose gate는 PASS이며 style profile은 측정값으로만 사용했다. 수치 맞추기용 문장 수정은 하지 않았다.",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:49+00:00",
"elapsedSeconds": 1,
"generation": 1,
"owner": "chatgpt-current-remediation",
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S6",
"name": "일한 사람의 목소리",
"skill": "writing-as-the-person-who-did-it",
"runBy": "voice-writer",
"status": "DONE",
"skipReason": "",
"skillEcho": "찾은 것이 없으면 **이 스킬은 여기서 끝난다.** 없는 목소리를 채우지 않는다.",
"skillRevision": "edd45dfec66d155a621cd41186b83b5582f2244c",
"inputs": [],
"outputs": [
"docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md"
],
"gates": [
{
"cmd": "node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:49+00:00"
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:49+00:00"
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S6/studio-body.md --frontend /shared/codebase/tech-log-frontend",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:49+00:00"
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs keycloak",
"exit": 0,
"session": "chatgpt-current-remediation",
"generation": 1,
"at": "2026-09-19T10:26:49+00:00"
}
],
"notes": "voice-writer 계약으로 현재 최종본을 다시 확인했다. 자료에 없는 경험 문장을 추가하지 않았고 Voice gate와 재실행 prose/body/evidence gate가 통과했다.",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:49+00:00",
"elapsedSeconds": 0,
"generation": 1,
"owner": "chatgpt-current-remediation",
"finishedBy": "chatgpt-current-remediation"
},
{
"id": "S7",
"name": "Studio 저장",
"skill": "publishing-tech-log-to-studio",
"runBy": "studio-validator",
"status": "SKIPPED",
"skipReason": "사용자가 이번 리뷰에서 Studio import/save를 요청하지 않았다. 기존 Studio 문서 version을 변경하지 않고 현재 저장소의 remediation 원장과 검증 결과만 남긴다.",
"skillEcho": "",
"skillRevision": "862e502af3e956b49ccd2ae0a8de3fd32f90df9c",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "",
"startedAt": null,
"finishedAt": "2026-09-19T10:26:49+00:00",
"elapsedSeconds": 0,
"finishedBy": "chatgpt-current-remediation"
}
],
"qualityReviews": {
"commandPedagogy": {
"initialAnalysis": {
"cmd": "python3 scripts/check-command-pedagogy.py --mode reference docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md -o runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S3/command-initial.json",
"exit": 0,
"shellBlocks": 0,
"findings": 0,
"majorFindings": 0,
"artifact": {
"path": "runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S3/command-initial.json",
"sha256": "eb41d2a40e9289239e4235f05531f0f9539074bb15629cb0f3971a198f3e36f2"
}
},
"finalAnalysis": {
"cmd": "python3 scripts/check-command-pedagogy.py --mode reference docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md -o runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S6/command-final.json",
"exit": 0,
"shellBlocks": 0,
"findings": 0,
"majorFindings": 0,
"artifact": {
"path": "runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/S6/command-final.json",
"sha256": "eb41d2a40e9289239e4235f05531f0f9539074bb15629cb0f3971a198f3e36f2"
}
},
"planner": {
"runBy": "command-pedagogy-planner",
"skill": "writing-practitioner-guides",
"status": "SKIPPED",
"skipReason": "결정론적 command analysis에서 shell block 0 · finding 0으로 판정되어 이 역할이 필요하지 않다.",
"skillEcho": "",
"skillRevision": null,
"notes": "initial/final analysis artifact로 shell/CLI가 없음을 확인했다.",
"artifact": null
},
"editor": {
"runBy": "command-pedagogy-editor",
"skill": "writing-practitioner-guides",
"status": "SKIPPED",
"skipReason": "결정론적 command analysis에서 shell block 0 · finding 0으로 판정되어 이 역할이 필요하지 않다.",
"skillEcho": "",
"skillRevision": null,
"notes": "initial/final analysis artifact로 shell/CLI가 없음을 확인했다.",
"artifact": null
},
"reviewer": {
"runBy": "command-pedagogy-reviewer",
"skill": "writing-practitioner-guides",
"status": "SKIPPED",
"skipReason": "결정론적 command analysis에서 shell block 0 · finding 0으로 판정되어 이 역할이 필요하지 않다.",
"skillEcho": "",
"skillRevision": null,
"verdict": null,
"notes": "initial/final analysis artifact로 shell/CLI가 없음을 확인했다.",
"artifact": null,
"sourceSha256": null
}
},
"technicalEvidence": {
"runBy": "fact-reviewer",
"status": "DONE",
"skipReason": "",
"verdict": "PASS",
"notes": "현재 최종 파일 hash를 기준으로 SSOT/tree/local evidence를 재대조했다. live source reconciliation = UNVERIFIABLE: /home/donghyeon/workspace/keycloak-pattern 이 현재 머신에 없다. 별도 Agent tool은 노출되지 않아 current remediation 세션이 fact-reviewer 계약을 직접 수행했다.",
"sourceSha256": "8a1643617fdfc6dad59bc52b50988b468fdf391b322639d89434ce1258347a3d",
"artifact": {
"path": "runs/keycloak/2026-09-19-1926-remediation-reference-pattern-selection/stage/quality/technical-evidence-review.json",
"sha256": "b959bb94a9372317825d58baeb1d25f6402c0a9b7a3ad9d052583c90f33f3b0f"
}
}
},
"riders": [],
"sessions": [
{
"session": "chatgpt-current-remediation",
"openedAt": "2026-09-19T10:26:47+00:00"
},
{
"session": "chatgpt-current-remediation",
"stage": "S3",
"generation": 1,
"beganAt": "2026-09-19T10:26:47+00:00"
},
{
"session": "chatgpt-current-remediation",
"stage": "S5",
"generation": 1,
"beganAt": "2026-09-19T10:26:48+00:00"
},
{
"session": "chatgpt-current-remediation",
"stage": "S6",
"generation": 1,
"beganAt": "2026-09-19T10:26:49+00:00"
}
],
"revision": 23,
"updatedAt": "2026-09-19T10:26:49+00:00",
"executionEnvironment": {
"mode": "current-remediation-contract-replay",
"session": "chatgpt-current-remediation",
"agentToolAvailable": false,
"note": "별도 Agent(subagent_type) 실행 도구가 현재 ChatGPT/Coka 환경에 노출되지 않았다. current remediation 세션이 .claude/agents 역할 계약과 각 SKILL.md를 읽고 동일한 gate를 현재 파일에 직접 실행했다. runBy는 verifier 계약 역할명이며 별도 Agent 프로세스 실행을 주장하지 않는다."
}
}
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"authority": "deterministic",
"gate": "command-pedagogy-signals",
"section_id": "docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"mode": "reference",
"source_sha256": "8a1643617fdfc6dad59bc52b50988b468fdf391b322639d89434ce1258347a3d",
"result": "PASS",
"requires_editor": false,
"blocks": [],
"findings": [],
"extensions": {
"command_like_text_blocks": []
}
}
@@ -0,0 +1,102 @@
---
id: 3f886154-1b85-407b-bda4-57d28370e745
kind: REFERENCE
slug: oauth-oidc-pattern-selection-criteria
title: OAuth/OIDC 인증 패턴 선택 기준
topic: oauth-oidc-auth-boundary
topicName: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 23
verifiedOn: 2026-08-30
studio: "https://hyeonworks.com/studio/documents/3f886154-1b85-407b-bda4-57d28370e745/edit"
public: "https://hyeonworks.com/references/oauth-oidc-pattern-selection-criteria"
sourceRevision: keycloak-patterns-lab@2026-08
source:
- final/document.md#검토한-선택지와-막힌-지점-책임과-데이터
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로
---
# OAuth/OIDC 인증 패턴 선택 기준
SPA(Single Page Application), Mediator, BFF(Backend for Frontend), Forward-Auth는 토큰과 인증 상태를 다루는 방식이 서로 다르다. 브라우저가 액세스 토큰을 직접 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 보관하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF(Cross-Site Request Forgery)를 어느 계층에서 처리하는지를 나란히 놓고 비교할 수 있다. 애플리케이션의 요구사항과 배포·운영 환경에 맞춰 고른다.
## 관계
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
브라우저가 인가 코드를 토큰으로 바꾸고, 그 토큰을 들고 있다가, API까지 직접 호출한다.
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
Mediator가 리프레시 토큰을 서버에 두는데, 브라우저는 넘겨받은 액세스 토큰으로 Resource Server를 직접 호출한다.
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
BFF가 인가 코드 교환과 토큰 보관, Resource Server 호출을 모두 처리하고 브라우저는 세션 쿠키만 받는다.
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
인증을 엣지로 옮기면 보호 자원이 검증하는 대상이 JWT에서 헤더로 바뀐다.
## 목적
브라우저에 OAuth 토큰이 노출되는 정도만 놓고 보면 구조마다 차이가 난다. 다만 토큰을 다른 계층으로 옮기면 브라우저에 노출되는 범위가 달라지는데, 그 토큰을 맡은 계층에서는 처리해야 할 항목이 늘어난다.
예를 들어 BFF는 OAuth 토큰을 서버에 보관해 브라우저에서 토큰 원문을 없앨 수 있다. 하지만 그러려면 서버가 세션과 Authorized Client를 관리해야 한다. Authorized Client는 서버가 액세스 토큰과 리프레시 토큰을 보관하는 곳이다. 그래서 세션 보호와 CSRF 방어, 공유 저장소 같은 설계가 새로 필요해진다.
Forward-Auth 구조에서는 애플리케이션이 OAuth 토큰을 직접 관리하는 책임을 더 줄일 수 있다. 대신 애플리케이션이 엣지에서 넘어온 사용자 정보 헤더를 신뢰하게 되므로, 헤더 위조 방지와 직접 접근 차단, 신뢰할 수 있는 네트워크 경계를 구성해야 한다.
선택 기준은 어느 구조가 더 안전한지에 대한 단일 순위가 아니라, 요구사항마다 달라지는 자격 증명 위치와 운영 책임이다.
## 규칙
### 1. 다섯 항목으로 구조를 비교한다
브라우저가 액세스 토큰을 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 관리하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF를 어디에서 처리하는지를 확인한다.
비교 단위는 패턴 이름이 아니라 요청 한 번의 실제 경로다. 비교 입력에는 엔드포인트와 메서드, 중간에 생기는 자격 증명, 성공·실패 응답이 포함되고 로그인 구간과 로그인 뒤 API 호출 구간을 각각 나눠 본다.
SPA와 Mediator에서는 브라우저가 액세스 토큰으로 Resource Server를 직접 호출한다. SPA는 Bearer 액세스 토큰을 Authorization 헤더에 직접 넣고 인증에는 쿠키를 쓰지 않는다. Mediator는 로그인 세션과 OAuth 토큰을 서버에서도 관리하고, 로그인이 끝나면 브라우저에 액세스 토큰을 전달한다.
BFF에서는 브라우저가 세션 쿠키로 BFF를 호출하고, BFF가 서버에 저장한 액세스 토큰으로 Resource Server를 호출한다. 그래서 브라우저에는 OAuth 토큰을 전달하지 않지만 세션과 Authorized Client를 서버에서 관리해야 한다.
Forward-Auth에서는 인증 프록시가 세션을 관리하고, 인증이 끝난 요청에 사용자 정보를 붙여 애플리케이션으로 넘긴다. 애플리케이션이 이 정보를 인증 근거로 쓴다면 엣지가 붙인 헤더를 믿을 수 있도록 직접 접근 차단과 헤더 덮어쓰기, 내부 자격 증명 검증 같은 보호를 따로 둬야 한다.
### 2. 피해야 할 조건을 먼저 확인한다
정책상 OAuth 토큰을 브라우저에 둘 수 없다면, 토큰을 Local Storage 대신 JavaScript 메모리에만 보관해도 요구사항을 채우지 못한다. 저장 위치만 달라졌을 뿐 브라우저 JavaScript가 여전히 토큰을 직접 다루기 때문이다. 이때는 브라우저가 액세스 토큰을 받는 SPA와 현재의 Mediator 구조를 선택 대상에서 뺀다.
마찬가지로 애플리케이션으로 바로 들어오는 경로를 막을 수 없거나, 밖에서 들어온 사용자 정보 헤더를 엣지에서 확실히 지우거나 덮어쓸 수 없다면, 엣지가 전달한 사용자 정보를 인증 근거로 쓰는 구조는 고르지 않는다.
### 3. 선택 조건과 운영 책임을 같이 문서화한다
선택 기록에는 구조 이름과 함께 그 선택을 만든 보안 요구사항과 운영 조건이 들어간다.
적용이 어려운 조건도 선택 기준의 일부다. 브라우저에 OAuth 토큰을 둘 수 없는 환경에서는 SPA를 고르기 어렵고, 애플리케이션 직접 경로나 사용자 정보 헤더를 통제할 수 없는 환경에서는 Forward-Auth를 적용하기 어렵다.
### 4. 이름으로 운영 속성을 추정하지 않는다
운영 조건에는 서버 재시작이나 인스턴스 장애 뒤 로그인 유지 여부, 여러 레플리카의 세션·토큰 상태 공유 방식, 저장소 장애 복구 방식이 포함된다.
내부 자격 증명과 암호화 키 같은 비밀값의 보관·교체 방식도 같은 운영 조건에 속한다.
### 5. 자격 증명의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다
패턴이 바뀌면 저장·전달·검증 책임도 다른 계층으로 이동한다.
예를 들어 Forward-Auth 구조에서는 엣지가 인증된 사용자 정보를 헤더로 애플리케이션에 전달할 수 있다. 처음에는 사용자 이름이나 이메일처럼 인증에 필요한 정보만 전달하더라도, 애플리케이션의 요구사항이 늘면서 역할이나 권한, 도메인에 묶인 사용자 정보까지 헤더에 계속 붙을 수 있다.
엣지가 전달할 정보가 역할·권한·도메인 정보까지 늘어나고 여러 API 응답의 조합과 인가 판단도 필요해지면, BFF가 인가와 API 호출을 소유하는 구성이 비교 대상이 된다.
## 적용 조건
- 인증 구조를 처음 고를 때
- 한 구조에서 다른 구조로 옮기려 할 때
- 구조를 문서로 비교할 때
## 예외
- 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면, 브라우저에 OAuth token을 둘 수 없고 server-side API composition이 필요할 때 AP3 BFF가 해당 조건을 만족한다.
- 학습이나 시연이 목적이면 운영 속성까지 비교하지 않아도 된다.
## 예시
- SPA: 브라우저가 인가 코드 교환과 토큰 보관, API 호출을 모두 맡는다.
- Mediator: 리프레시 토큰은 서버에 두고, 액세스 토큰은 응답 본문으로 브라우저에 돌려준다.
- BFF: 서버가 인가 코드 교환과 토큰 관리, API 호출을 맡고 브라우저는 세션 쿠키로 BFF를 호출한다.
- Forward-Auth: 엣지가 인증하고 애플리케이션은 엣지가 붙인 헤더를 본다.
@@ -0,0 +1,102 @@
---
id: 3f886154-1b85-407b-bda4-57d28370e745
kind: REFERENCE
slug: oauth-oidc-pattern-selection-criteria
title: OAuth/OIDC 인증 패턴 선택 기준
topic: oauth-oidc-auth-boundary
topicName: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 23
verifiedOn: 2026-08-30
studio: "https://hyeonworks.com/studio/documents/3f886154-1b85-407b-bda4-57d28370e745/edit"
public: "https://hyeonworks.com/references/oauth-oidc-pattern-selection-criteria"
sourceRevision: keycloak-patterns-lab@2026-08
source:
- final/document.md#검토한-선택지와-막힌-지점-책임과-데이터
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로
---
# OAuth/OIDC 인증 패턴 선택 기준
SPA(Single Page Application), Mediator, BFF(Backend for Frontend), Forward-Auth는 토큰과 인증 상태를 다루는 방식이 서로 다르다. 브라우저가 액세스 토큰을 직접 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 보관하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF(Cross-Site Request Forgery)를 어느 계층에서 처리하는지를 나란히 놓고 비교할 수 있다. 애플리케이션의 요구사항과 배포·운영 환경에 맞춰 고른다.
## 관계
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
브라우저가 인가 코드를 토큰으로 바꾸고, 그 토큰을 들고 있다가, API까지 직접 호출한다.
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
Mediator가 리프레시 토큰을 서버에 두는데, 브라우저는 넘겨받은 액세스 토큰으로 Resource Server를 직접 호출한다.
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
BFF가 인가 코드 교환과 토큰 보관, Resource Server 호출을 모두 처리하고 브라우저는 세션 쿠키만 받는다.
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
인증을 엣지로 옮기면 보호 자원이 검증하는 대상이 JWT에서 헤더로 바뀐다.
## 목적
브라우저에 OAuth 토큰이 노출되는 정도만 놓고 보면 구조마다 차이가 난다. 다만 토큰을 다른 계층으로 옮기면 브라우저에 노출되는 범위가 달라지는데, 그 토큰을 맡은 계층에서는 처리해야 할 항목이 늘어난다.
예를 들어 BFF는 OAuth 토큰을 서버에 보관해 브라우저에서 토큰 원문을 없앨 수 있다. 하지만 그러려면 서버가 세션과 Authorized Client를 관리해야 한다. Authorized Client는 서버가 액세스 토큰과 리프레시 토큰을 보관하는 곳이다. 그래서 세션 보호와 CSRF 방어, 공유 저장소 같은 설계가 새로 필요해진다.
Forward-Auth 구조에서는 애플리케이션이 OAuth 토큰을 직접 관리하는 책임을 더 줄일 수 있다. 대신 애플리케이션이 엣지에서 넘어온 사용자 정보 헤더를 신뢰하게 되므로, 헤더 위조 방지와 직접 접근 차단, 신뢰할 수 있는 네트워크 경계를 구성해야 한다.
선택 기준은 어느 구조가 더 안전한지에 대한 단일 순위가 아니라, 요구사항마다 달라지는 자격 증명 위치와 운영 책임이다.
## 규칙
### 1. 다섯 항목으로 구조를 비교한다
브라우저가 액세스 토큰을 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 관리하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF를 어디에서 처리하는지를 확인한다.
비교 단위는 패턴 이름이 아니라 요청 한 번의 실제 경로다. 비교 입력에는 엔드포인트와 메서드, 중간에 생기는 자격 증명, 성공·실패 응답이 포함되고 로그인 구간과 로그인 뒤 API 호출 구간을 각각 나눠 본다.
SPA와 Mediator에서는 브라우저가 액세스 토큰으로 Resource Server를 직접 호출한다. SPA는 Bearer 액세스 토큰을 Authorization 헤더에 직접 넣고 인증에는 쿠키를 쓰지 않는다. Mediator는 로그인 세션과 OAuth 토큰을 서버에서도 관리하고, 로그인이 끝나면 브라우저에 액세스 토큰을 전달한다.
BFF에서는 브라우저가 세션 쿠키로 BFF를 호출하고, BFF가 서버에 저장한 액세스 토큰으로 Resource Server를 호출한다. 그래서 브라우저에는 OAuth 토큰을 전달하지 않지만 세션과 Authorized Client를 서버에서 관리해야 한다.
Forward-Auth에서는 인증 프록시가 세션을 관리하고, 인증이 끝난 요청에 사용자 정보를 붙여 애플리케이션으로 넘긴다. 애플리케이션이 이 정보를 인증 근거로 쓴다면 엣지가 붙인 헤더를 믿을 수 있도록 직접 접근 차단과 헤더 덮어쓰기, 내부 자격 증명 검증 같은 보호를 따로 둬야 한다.
### 2. 피해야 할 조건을 먼저 확인한다
정책상 OAuth 토큰을 브라우저에 둘 수 없다면, 토큰을 Local Storage 대신 JavaScript 메모리에만 보관해도 요구사항을 채우지 못한다. 저장 위치만 달라졌을 뿐 브라우저 JavaScript가 여전히 토큰을 직접 다루기 때문이다. 이때는 브라우저가 액세스 토큰을 받는 SPA와 현재의 Mediator 구조를 선택 대상에서 뺀다.
마찬가지로 애플리케이션으로 바로 들어오는 경로를 막을 수 없거나, 밖에서 들어온 사용자 정보 헤더를 엣지에서 확실히 지우거나 덮어쓸 수 없다면, 엣지가 전달한 사용자 정보를 인증 근거로 쓰는 구조는 고르지 않는다.
### 3. 선택 조건과 운영 책임을 같이 문서화한다
선택 기록에는 구조 이름과 함께 그 선택을 만든 보안 요구사항과 운영 조건이 들어간다.
적용이 어려운 조건도 선택 기준의 일부다. 브라우저에 OAuth 토큰을 둘 수 없는 환경에서는 SPA를 고르기 어렵고, 애플리케이션 직접 경로나 사용자 정보 헤더를 통제할 수 없는 환경에서는 Forward-Auth를 적용하기 어렵다.
### 4. 이름으로 운영 속성을 추정하지 않는다
운영 조건에는 서버 재시작이나 인스턴스 장애 뒤 로그인 유지 여부, 여러 레플리카의 세션·토큰 상태 공유 방식, 저장소 장애 복구 방식이 포함된다.
내부 자격 증명과 암호화 키 같은 비밀값의 보관·교체 방식도 같은 운영 조건에 속한다.
### 5. 자격 증명의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다
패턴이 바뀌면 저장·전달·검증 책임도 다른 계층으로 이동한다.
예를 들어 Forward-Auth 구조에서는 엣지가 인증된 사용자 정보를 헤더로 애플리케이션에 전달할 수 있다. 처음에는 사용자 이름이나 이메일처럼 인증에 필요한 정보만 전달하더라도, 애플리케이션의 요구사항이 늘면서 역할이나 권한, 도메인에 묶인 사용자 정보까지 헤더에 계속 붙을 수 있다.
엣지가 전달할 정보가 역할·권한·도메인 정보까지 늘어나고 여러 API 응답의 조합과 인가 판단도 필요해지면, BFF가 인가와 API 호출을 소유하는 구성이 비교 대상이 된다.
## 적용 조건
- 인증 구조를 처음 고를 때
- 한 구조에서 다른 구조로 옮기려 할 때
- 구조를 문서로 비교할 때
## 예외
- 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면, 브라우저에 OAuth token을 둘 수 없고 server-side API composition이 필요할 때 AP3 BFF가 해당 조건을 만족한다.
- 학습이나 시연이 목적이면 운영 속성까지 비교하지 않아도 된다.
## 예시
- SPA: 브라우저가 인가 코드 교환과 토큰 보관, API 호출을 모두 맡는다.
- Mediator: 리프레시 토큰은 서버에 두고, 액세스 토큰은 응답 본문으로 브라우저에 돌려준다.
- BFF: 서버가 인가 코드 교환과 토큰 관리, API 호출을 맡고 브라우저는 세션 쿠키로 BFF를 호출한다.
- Forward-Auth: 엣지가 인증하고 애플리케이션은 엣지가 붙인 헤더를 본다.
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"authority": "deterministic",
"gate": "command-pedagogy-signals",
"section_id": "docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"mode": "reference",
"source_sha256": "8a1643617fdfc6dad59bc52b50988b468fdf391b322639d89434ce1258347a3d",
"result": "PASS",
"requires_editor": false,
"blocks": [],
"findings": [],
"extensions": {
"command_like_text_blocks": []
}
}
@@ -0,0 +1,102 @@
---
id: 3f886154-1b85-407b-bda4-57d28370e745
kind: REFERENCE
slug: oauth-oidc-pattern-selection-criteria
title: OAuth/OIDC 인증 패턴 선택 기준
topic: oauth-oidc-auth-boundary
topicName: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 23
verifiedOn: 2026-08-30
studio: "https://hyeonworks.com/studio/documents/3f886154-1b85-407b-bda4-57d28370e745/edit"
public: "https://hyeonworks.com/references/oauth-oidc-pattern-selection-criteria"
sourceRevision: keycloak-patterns-lab@2026-08
source:
- final/document.md#검토한-선택지와-막힌-지점-책임과-데이터
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로
---
# OAuth/OIDC 인증 패턴 선택 기준
SPA(Single Page Application), Mediator, BFF(Backend for Frontend), Forward-Auth는 토큰과 인증 상태를 다루는 방식이 서로 다르다. 브라우저가 액세스 토큰을 직접 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 보관하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF(Cross-Site Request Forgery)를 어느 계층에서 처리하는지를 나란히 놓고 비교할 수 있다. 애플리케이션의 요구사항과 배포·운영 환경에 맞춰 고른다.
## 관계
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
브라우저가 인가 코드를 토큰으로 바꾸고, 그 토큰을 들고 있다가, API까지 직접 호출한다.
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
Mediator가 리프레시 토큰을 서버에 두는데, 브라우저는 넘겨받은 액세스 토큰으로 Resource Server를 직접 호출한다.
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
BFF가 인가 코드 교환과 토큰 보관, Resource Server 호출을 모두 처리하고 브라우저는 세션 쿠키만 받는다.
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
인증을 엣지로 옮기면 보호 자원이 검증하는 대상이 JWT에서 헤더로 바뀐다.
## 목적
브라우저에 OAuth 토큰이 노출되는 정도만 놓고 보면 구조마다 차이가 난다. 다만 토큰을 다른 계층으로 옮기면 브라우저에 노출되는 범위가 달라지는데, 그 토큰을 맡은 계층에서는 처리해야 할 항목이 늘어난다.
예를 들어 BFF는 OAuth 토큰을 서버에 보관해 브라우저에서 토큰 원문을 없앨 수 있다. 하지만 그러려면 서버가 세션과 Authorized Client를 관리해야 한다. Authorized Client는 서버가 액세스 토큰과 리프레시 토큰을 보관하는 곳이다. 그래서 세션 보호와 CSRF 방어, 공유 저장소 같은 설계가 새로 필요해진다.
Forward-Auth 구조에서는 애플리케이션이 OAuth 토큰을 직접 관리하는 책임을 더 줄일 수 있다. 대신 애플리케이션이 엣지에서 넘어온 사용자 정보 헤더를 신뢰하게 되므로, 헤더 위조 방지와 직접 접근 차단, 신뢰할 수 있는 네트워크 경계를 구성해야 한다.
선택 기준은 어느 구조가 더 안전한지에 대한 단일 순위가 아니라, 요구사항마다 달라지는 자격 증명 위치와 운영 책임이다.
## 규칙
### 1. 다섯 항목으로 구조를 비교한다
브라우저가 액세스 토큰을 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 관리하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF를 어디에서 처리하는지를 확인한다.
비교 단위는 패턴 이름이 아니라 요청 한 번의 실제 경로다. 비교 입력에는 엔드포인트와 메서드, 중간에 생기는 자격 증명, 성공·실패 응답이 포함되고 로그인 구간과 로그인 뒤 API 호출 구간을 각각 나눠 본다.
SPA와 Mediator에서는 브라우저가 액세스 토큰으로 Resource Server를 직접 호출한다. SPA는 Bearer 액세스 토큰을 Authorization 헤더에 직접 넣고 인증에는 쿠키를 쓰지 않는다. Mediator는 로그인 세션과 OAuth 토큰을 서버에서도 관리하고, 로그인이 끝나면 브라우저에 액세스 토큰을 전달한다.
BFF에서는 브라우저가 세션 쿠키로 BFF를 호출하고, BFF가 서버에 저장한 액세스 토큰으로 Resource Server를 호출한다. 그래서 브라우저에는 OAuth 토큰을 전달하지 않지만 세션과 Authorized Client를 서버에서 관리해야 한다.
Forward-Auth에서는 인증 프록시가 세션을 관리하고, 인증이 끝난 요청에 사용자 정보를 붙여 애플리케이션으로 넘긴다. 애플리케이션이 이 정보를 인증 근거로 쓴다면 엣지가 붙인 헤더를 믿을 수 있도록 직접 접근 차단과 헤더 덮어쓰기, 내부 자격 증명 검증 같은 보호를 따로 둬야 한다.
### 2. 피해야 할 조건을 먼저 확인한다
정책상 OAuth 토큰을 브라우저에 둘 수 없다면, 토큰을 Local Storage 대신 JavaScript 메모리에만 보관해도 요구사항을 채우지 못한다. 저장 위치만 달라졌을 뿐 브라우저 JavaScript가 여전히 토큰을 직접 다루기 때문이다. 이때는 브라우저가 액세스 토큰을 받는 SPA와 현재의 Mediator 구조를 선택 대상에서 뺀다.
마찬가지로 애플리케이션으로 바로 들어오는 경로를 막을 수 없거나, 밖에서 들어온 사용자 정보 헤더를 엣지에서 확실히 지우거나 덮어쓸 수 없다면, 엣지가 전달한 사용자 정보를 인증 근거로 쓰는 구조는 고르지 않는다.
### 3. 선택 조건과 운영 책임을 같이 문서화한다
선택 기록에는 구조 이름과 함께 그 선택을 만든 보안 요구사항과 운영 조건이 들어간다.
적용이 어려운 조건도 선택 기준의 일부다. 브라우저에 OAuth 토큰을 둘 수 없는 환경에서는 SPA를 고르기 어렵고, 애플리케이션 직접 경로나 사용자 정보 헤더를 통제할 수 없는 환경에서는 Forward-Auth를 적용하기 어렵다.
### 4. 이름으로 운영 속성을 추정하지 않는다
운영 조건에는 서버 재시작이나 인스턴스 장애 뒤 로그인 유지 여부, 여러 레플리카의 세션·토큰 상태 공유 방식, 저장소 장애 복구 방식이 포함된다.
내부 자격 증명과 암호화 키 같은 비밀값의 보관·교체 방식도 같은 운영 조건에 속한다.
### 5. 자격 증명의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다
패턴이 바뀌면 저장·전달·검증 책임도 다른 계층으로 이동한다.
예를 들어 Forward-Auth 구조에서는 엣지가 인증된 사용자 정보를 헤더로 애플리케이션에 전달할 수 있다. 처음에는 사용자 이름이나 이메일처럼 인증에 필요한 정보만 전달하더라도, 애플리케이션의 요구사항이 늘면서 역할이나 권한, 도메인에 묶인 사용자 정보까지 헤더에 계속 붙을 수 있다.
엣지가 전달할 정보가 역할·권한·도메인 정보까지 늘어나고 여러 API 응답의 조합과 인가 판단도 필요해지면, BFF가 인가와 API 호출을 소유하는 구성이 비교 대상이 된다.
## 적용 조건
- 인증 구조를 처음 고를 때
- 한 구조에서 다른 구조로 옮기려 할 때
- 구조를 문서로 비교할 때
## 예외
- 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면, 브라우저에 OAuth token을 둘 수 없고 server-side API composition이 필요할 때 AP3 BFF가 해당 조건을 만족한다.
- 학습이나 시연이 목적이면 운영 속성까지 비교하지 않아도 된다.
## 예시
- SPA: 브라우저가 인가 코드 교환과 토큰 보관, API 호출을 모두 맡는다.
- Mediator: 리프레시 토큰은 서버에 두고, 액세스 토큰은 응답 본문으로 브라우저에 돌려준다.
- BFF: 서버가 인가 코드 교환과 토큰 관리, API 호출을 맡고 브라우저는 세션 쿠키로 BFF를 호출한다.
- Forward-Auth: 엣지가 인증하고 애플리케이션은 엣지가 붙인 헤더를 본다.
@@ -0,0 +1,25 @@
{
"record": "docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"sourceSha256": "8a1643617fdfc6dad59bc52b50988b468fdf391b322639d89434ce1258347a3d",
"verdict": "PASS",
"checks": [
{
"name": "required-content",
"cmd": "python3 scripts/check-required-content.py --file docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md",
"exit": 0
},
{
"name": "tree",
"cmd": "python3 scripts/verify-tech-log-tree.py keycloak",
"exit": 0
},
{
"name": "evidence-local",
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs keycloak",
"exit": 0
}
],
"liveSourceReconciliation": "UNVERIFIABLE",
"liveSourcePath": "/home/donghyeon/workspace/keycloak-pattern",
"note": "현재 최종 Record를 기존 SSOT/tree/evidence 계약에 다시 대조했다. source repository가 없으면 live reconciliation은 UNVERIFIABLE로 남긴다."
}