Files
document-haness/.run/keycloak-four-patterns/outline.json
T

280 lines
13 KiB
JSON

{
"title": "브라우저 토큰에서 엣지 세션까지: Keycloak 인증 패턴 네 가지의 경계 설계",
"document_type": "technical_blog",
"sections": [
{
"id": "01-problem-scene",
"intent": "problem_scene",
"title": "코드보다 먼저 드러난 문제",
"reader_question": "독자가 공감할 수 있는 구체적인 상황에서 어떤 문제가 드러났는가?",
"purpose": "추상적인 글쓰기 계약이 아니라 실제 장면, 증상, 비용으로 시작한다.",
"must_include": [
"구체적인 상황",
"문제가 만든 비용",
"이 글에서 풀 질문",
"네 패턴을 보안 등급이 아니라 OAuth 코드·토큰·세션·신뢰 헤더의 소유 위치로 비교하고 자신의 환경에 맞는 Keycloak 통합 경계를 선택할 수 있다",
"네 패턴의 차이는 로그인 화면이 아니라 OAuth 책임을 어디에 둘 것인가에 있다. 브라우저에서 mediator와 BFF를 거쳐 edge로 책임을 이동할수록 브라우저의 토큰 노출은 줄지만 서버 상태, CSRF, 프록시 헤더 신뢰 같은 다른 비용과 가드레일이 생긴다.",
"develop-keycloak-pattern1부터 develop-keycloak-pattern4까지의 브라우저 인증 구조",
"AP1 SPA direct, AP2 token mediator, AP3 BFF, AP4 edge forward-auth의 흐름",
"각 패턴의 선택 맥락, 대안, 수용 비용, 가드레일과 저장소 내 검증",
"Google federation이 네 패턴과 맺는 공통 관계",
"Keycloak 설치를 처음부터 따라 하는 튜토리얼",
"모든 조직에 적용되는 단일 최적 패턴",
"실제 Google 계정과 운영 트래픽을 사용한 운영 검증",
"성능·부하·장애 복구 수치 비교"
],
"evidence_ids": [
"L4121b8d86b",
"AP1_BOUNDARY",
"AP2_BOUNDARY",
"AP3_BOUNDARY",
"AP4_BOUNDARY"
],
"decision_requirements": [],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "02-constraints",
"intent": "constraints",
"title": "문제를 어렵게 만든 제약",
"reader_question": "단순한 해법을 막은 프로젝트 제약은 무엇이었는가?",
"purpose": "현재 구조, 독자에게 필요한 배경, 확인된 사실과 미확인 영역을 분리한다.",
"must_include": [
"현재 구조",
"제약",
"확인된 사실과 사실 경계"
],
"evidence_ids": [
"L4121b8d86b",
"AP1_STORAGE",
"AP2_GUARDRAILS",
"AP3_TRADEOFF",
"AP4_BOUNDARY",
"AP4_RESPONSE_RUNTIME"
],
"decision_requirements": [],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "03-options",
"intent": "options",
"title": "검토한 선택지와 막힌 지점",
"reader_question": "어떤 대안들을 검토했고 각각 어디에서 비용이 생겼는가?",
"purpose": "최소 두 선택지를 같은 기준으로 비교하고, 실패한 시도나 제외 이유를 숨기지 않는다.",
"must_include": [
"대안",
"비교 기준",
"제외 이유 또는 실패한 시도",
"패턴을 가르는 공통 질문: 누가 OAuth client이고 브라우저가 무엇을 보유하는가",
"AP1 public SPA와 PKCE, resource server, issuer·audience·role 검증",
"AP1 로그인 callback과 token set의 memory 저장, Bearer API 요청과 /api/me 응답까지의 input-output",
"AP2 confidential mediator와 access-only handoff, access token 경계",
"AP2 oauth2Login callback, authorized client 저장, /token/boundary와 /token/access의 정확한 응답, browser-to-API input-output",
"AP3 oauth2Login BFF와 서버 세션, CSRF·SameSite 방어",
"AP3 /bff/api/me의 authorized-client 조회와 downstream Bearer 호출, CSRF 발급과 preferences POST의 input-output",
"AP4 oauth2-proxy와 nginx auth_request, 신뢰 헤더 스푸핑 방어",
"AP4 외부 URL에서 내부 auth subrequest와 /edge/me로 이어지는 URL·cookie·identity header 변환과 input-output",
"각 패턴에서 browser, mediator 또는 BFF, edge, Resource Server가 실제로 보유하고 전달하는 데이터 목록",
"각 패턴의 실제 클래스·메서드·설정 호출 순서와 성공·실패 HTTP 결과",
"브라우저 token 노출과 서버 상태 사이의 트레이드오프",
"Google은 별도 패턴이 아니라 Keycloak 앞의 upstream IdP라는 경계",
"저장소 테스트가 확인한 범위와 확인하지 못한 범위"
],
"evidence_ids": [
"L4121b8d86b",
"AP1_BOUNDARY",
"AP1_STORAGE",
"AP2_BOUNDARY",
"AP2_IMPLEMENTATION",
"AP3_BOUNDARY",
"AP3_TRADEOFF",
"AP4_BOUNDARY",
"AP4_ALTERNATIVE"
],
"decision_requirements": [
"상황·제약",
"선택",
"선택 이유",
"검토한 대안",
"수용한 비용",
"보완 가드레일"
],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "04-decision-rationale",
"intent": "decision_rationale",
"title": "선택의 이유와 지킨 경계",
"reader_question": "왜 이 선택을 했으며 무엇을 일부러 포기하거나 금지했는가?",
"purpose": "선택을 제약, 이유, 대안, 수용 비용, 보완 가드레일까지 한 묶음으로 설명한다.",
"must_include": [
"선택",
"왜 선택했는가",
"대안",
"수용한 비용",
"가드레일"
],
"evidence_ids": [
"AP1_BOUNDARY",
"AP1_STORAGE",
"AP2_BOUNDARY",
"AP3_BOUNDARY",
"AP3_TRADEOFF",
"AP4_BOUNDARY",
"AP4_ALTERNATIVE"
],
"decision_requirements": [
"상황·제약",
"선택",
"선택 이유",
"검토한 대안",
"수용한 비용",
"보완 가드레일"
],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "05-mechanism",
"intent": "mechanism",
"title": "선택이 코드와 흐름에 반영되는 방식",
"reader_question": "결정이 모듈, 인터페이스, 제어 흐름에 어떻게 반영되는가?",
"purpose": "실제 이름과 경계를 사용해 인과 흐름을 설명하고, 하나의 구체적인 예시를 끝까지 따라간다.",
"must_include": [
"실제 구성요소",
"제어 또는 데이터 흐름",
"구체적인 예시",
"불변조건",
"패턴을 가르는 공통 질문: 누가 OAuth client이고 브라우저가 무엇을 보유하는가",
"AP1 public SPA와 PKCE, resource server, issuer·audience·role 검증",
"AP1 로그인 callback과 token set의 memory 저장, Bearer API 요청과 /api/me 응답까지의 input-output",
"AP2 confidential mediator와 access-only handoff, access token 경계",
"AP2 oauth2Login callback, authorized client 저장, /token/boundary와 /token/access의 정확한 응답, browser-to-API input-output",
"AP3 oauth2Login BFF와 서버 세션, CSRF·SameSite 방어",
"AP3 /bff/api/me의 authorized-client 조회와 downstream Bearer 호출, CSRF 발급과 preferences POST의 input-output",
"AP4 oauth2-proxy와 nginx auth_request, 신뢰 헤더 스푸핑 방어",
"AP4 외부 URL에서 내부 auth subrequest와 /edge/me로 이어지는 URL·cookie·identity header 변환과 input-output",
"각 패턴에서 browser, mediator 또는 BFF, edge, Resource Server가 실제로 보유하고 전달하는 데이터 목록",
"각 패턴의 실제 클래스·메서드·설정 호출 순서와 성공·실패 HTTP 결과",
"브라우저 token 노출과 서버 상태 사이의 트레이드오프",
"Google은 별도 패턴이 아니라 Keycloak 앞의 upstream IdP라는 경계",
"저장소 테스트가 확인한 범위와 확인하지 못한 범위"
],
"evidence_ids": [
"AP1_LOGIN_RUNTIME",
"AP1_PKCE_DEMO_GAP",
"AP1_API_RUNTIME",
"AP1_ROLE_FAILURE_RUNTIME",
"AP2_LOGIN_FLOW",
"AP2_BOUNDARY_RUNTIME",
"AP2_ACCESS_RUNTIME",
"AP2_RESOURCE_RUNTIME",
"AP3_LOGIN_FLOW",
"AP3_BOUNDARY_RUNTIME",
"AP3_API_RUNTIME",
"AP3_CSRF_RUNTIME",
"AP3_PREFERENCE_SCOPE",
"AP4_LOGIN_RUNTIME",
"AP4_REQUEST_RUNTIME",
"AP4_RESPONSE_RUNTIME"
],
"decision_requirements": [],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "06-evidence-verification",
"intent": "evidence_verification",
"title": "결정이 지켜지는지 확인하는 방법",
"reader_question": "설명한 경계와 결과가 실제로 유지되는지 어떻게 확인하는가?",
"purpose": "테스트, 빌드 규칙, 관측값을 주장과 연결하고 검증 범위를 과장하지 않는다.",
"must_include": [
"검증 절차",
"성공 기준",
"검증하지 못한 범위",
"패턴을 가르는 공통 질문: 누가 OAuth client이고 브라우저가 무엇을 보유하는가",
"AP1 public SPA와 PKCE, resource server, issuer·audience·role 검증",
"AP1 로그인 callback과 token set의 memory 저장, Bearer API 요청과 /api/me 응답까지의 input-output",
"AP2 confidential mediator와 access-only handoff, access token 경계",
"AP2 oauth2Login callback, authorized client 저장, /token/boundary와 /token/access의 정확한 응답, browser-to-API input-output",
"AP3 oauth2Login BFF와 서버 세션, CSRF·SameSite 방어",
"AP3 /bff/api/me의 authorized-client 조회와 downstream Bearer 호출, CSRF 발급과 preferences POST의 input-output",
"AP4 oauth2-proxy와 nginx auth_request, 신뢰 헤더 스푸핑 방어",
"AP4 외부 URL에서 내부 auth subrequest와 /edge/me로 이어지는 URL·cookie·identity header 변환과 input-output",
"각 패턴에서 browser, mediator 또는 BFF, edge, Resource Server가 실제로 보유하고 전달하는 데이터 목록",
"각 패턴의 실제 클래스·메서드·설정 호출 순서와 성공·실패 HTTP 결과",
"브라우저 token 노출과 서버 상태 사이의 트레이드오프",
"Google은 별도 패턴이 아니라 Keycloak 앞의 upstream IdP라는 경계",
"저장소 테스트가 확인한 범위와 확인하지 못한 범위"
],
"evidence_ids": [
"AP1_VERIFY",
"AP1_ROLE_FAILURE_RUNTIME",
"AP2_VERIFY",
"AP2_ACCESS_RUNTIME",
"AP3_VERIFY",
"AP3_CSRF_RUNTIME",
"AP4_VERIFY",
"AP4_RESPONSE_RUNTIME",
"BRANCH_REACHABILITY"
],
"decision_requirements": [],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "07-tradeoffs",
"intent": "tradeoffs",
"title": "얻은 것, 잃은 것, 적용하지 않을 때",
"reader_question": "이 선택의 비용과 한계는 무엇이며 언제 다른 선택이 나은가?",
"purpose": "프로젝트 지역 결정을 보편 법칙처럼 쓰지 않고, 적용 조건과 남은 위험을 제시한다.",
"must_include": [
"얻은 것",
"잃은 것",
"적용 조건",
"남은 위험"
],
"evidence_ids": [
"L4121b8d86b",
"AP1_STORAGE",
"AP2_GUARDRAILS",
"AP3_TRADEOFF",
"AP3_PREFERENCE_SCOPE",
"AP4_BOUNDARY",
"AP4_ALTERNATIVE"
],
"decision_requirements": [
"상황·제약",
"선택",
"선택 이유",
"검토한 대안",
"수용한 비용",
"보완 가드레일"
],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
},
{
"id": "08-conclusion",
"intent": "conclusion",
"title": "결국 지키려던 것은 무엇이었나",
"reader_question": "세부 기술을 걷어냈을 때 남는 판단은 무엇인가?",
"purpose": "앞 내용을 반복하지 않고, 문제와 선택을 연결하는 한 문장 판단으로 닫는다.",
"must_include": [
"압축된 판단",
"독자가 자신의 환경에서 확인할 질문"
],
"evidence_ids": [
"L4121b8d86b",
"AP1_BOUNDARY",
"AP2_BOUNDARY",
"AP3_BOUNDARY",
"AP4_BOUNDARY"
],
"decision_requirements": [],
"transition_to_next": "이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
}
],
"planning_notes": [
"Each section answers one reader question.",
"The order moves from reader goal to context, model, mechanism, evidence, limits, and action as applicable.",
"Required section intents are a contract; a model may refine wording but must not remove or reorder them."
]
}