280 lines
13 KiB
JSON
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."
|
|
]
|
|
}
|