{ "schemaVersion": 4, "project": "keycloak", "ssot": "final/document.md", "sourceRepository": { "path": "/home/donghyeon/workspace/keycloak-pattern", "revision": null, "revisions": { "AP1 develop-keycloak-pattern1": "64175266df05545f8f181fc91c1f0364bc47fce9", "AP2 develop-keycloak-pattern2": "d019846f8725bdb0badde33043b020dc252e32ff", "AP3 develop-keycloak-pattern3": "934c5da5d6edc2429dfb558b773656e46f21d677", "AP4 develop-keycloak-pattern4": "f4aea65dc6255eae07b20ebbe21e02fb6115e563" }, "verified": "네 패턴이 각각 다른 브랜치에 있어 단일 커밋으로 표현되지 않는다. 각 tip 에서 문서가 인용한 것을 찾아 대조했다 — pattern1 의 frontend/src/app.js 에 InMemoryWebStorage 와 redirect_uri http://localhost:8088/callback.html, pattern2 의 /token/access, pattern3 의 BffController /bff/token-boundary, pattern4 의 EdgeIdentityController /edge/me. 공통 realm(keycloak/import/keycloak-patterns-realm.json)의 client 넷과 mock-google-realm.json 도 확인했다. 저장소 HEAD 는 keycloak-session-store 작업이라 이 문서의 상태가 아니다 (2026-09-07 확인)" }, "ssotSha256": "0625bc875ab31ca6f331e6f64bd93f26397d54f3ac6e0162f4128a53a1d97e2d", "sourceRevision": "keycloak-patterns-lab@2026-08", "generatedAt": "2026-09-07", "candidateScope": { "document": "final/document.md", "sections": [ "문제를 어렵게 만든 제약", "검토한 선택지와 막힌 지점", "선택의 이유와 지킨 경계", "선택이 코드와 흐름에 반영되는 방식", "결정이 지켜지는지 확인하는 방법", "얻은 것, 잃은 것, 적용하지 않을 때" ], "excluded": [ "코드보다 먼저 드러난 문제", "결국 지키려던 것은 무엇이었나" ], "note": "처음부터 한 편으로 쓴 글이라 제2부가 없다. 여는 절은 문제 진술이고 맺음 절은 앞 절들의 종합이라 후보 자리가 아니다" }, "note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸은 사람이 적고 file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py keycloak", "contract": { "decomposition": [ "글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.", "후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.", "없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.", "Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.", "Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다." ], "readinessValues": [ "READY", "OPEN", "NEEDS_EVIDENCE", "NEEDS_DECISION", "BLOCKED" ], "dispositionValues": { "PROMOTE": "독립 Tech Log 로 쓴다", "MERGE_INTO": "다른 기록의 한 절로 흡수한다", "KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다", "NEEDS_EVIDENCE": "주장에 아직 검증이 없다", "NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다", "BLOCKED": "원본이 불완전하거나 서로 어긋난다" } }, "topics": { "oauth-oidc-auth-boundary": { "topic": "oauth-oidc-auth-boundary", "title": "OAuth 자격증명과 세션의 보관 경계", "readerQuestion": "자격증명과 세션을 누가 보관하고, 누가 API 요청을 만들며, 보호 자원은 무엇을 신뢰하는가?", "kinds": { "case": [ { "title": "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우", "slug": "spa-browser-credential-boundary", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap1", "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#결정이-지켜지는지-확인하는-방법-ap1" ], "code": [ "spa-public client", "InMemoryWebStorage" ], "classification": "한 패턴을 끝까지 따라가 토큰이 어디에 남는지 관측하고, 저장소를 줄여도 실행 중 XSS 권한은 줄지 않는다는 결론까지 닫았다", "missing-verification": "silent renewal, SSO cookie flag, CORS preflight, callback error UX 는 이 검증 계약 밖이다", "relations": [ "concept:browser-credential-storage", "concept:authorization-code-and-pkce", "concept:bearer-jwt-validation-chain", "reference:public-confidential-client-boundary" ], "kind": "case", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/case/case-browser-credential-boundary.md", "status": "게시 중", "studioId": "bf675775-4f3e-4744-8014-f0efff51422a", "assets": [ "ap1-custody-v3-6e0376d2" ], "evidenceFiles": [] }, { "title": "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출", "slug": "split-custody-access-token", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap2", "final/document.md#선택의-이유와-지킨-경계-ap2", "final/document.md#결정이-지켜지는지-확인하는-방법-ap2" ], "code": [ "Spring oauth2Login mediator", "/token/access", "/token/boundary" ], "classification": "one-time handoff 가 되는지 확인했고 access-only handoff 까지만 성립한다는 범위로 닫았다", "missing-verification": "access token 전달 횟수 제한, durable store, logout, 만료 뒤 실제 refresh 는 검증하지 못했다", "relations": [ "case:spa-browser-credential-boundary", "case:bff-session-csrf-responsibility", "reference:oauth-token-application-session-boundary", "question:refresh-rotation-replica-contention" ], "kind": "case", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/case/case-ap2-split-custody.md", "status": "게시 중", "studioId": "488ce49b-afa4-42a5-a2ce-de2e0653cd82", "assets": [ "ap2-split-custody-779cb791" ], "evidenceFiles": [] }, { "title": "BFF에서 Browser Token을 제거하고 Session과 CSRF를 처리한 방식", "slug": "bff-session-csrf-responsibility", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#결정이-지켜지는지-확인하는-방법-ap3" ], "code": [ "bff-confidential client", "XSRF-TOKEN", "/bff/token-boundary" ], "classification": "브라우저에서 토큰이 사라진 대신 서버가 무엇을 떠안았는지를 세 요청으로 재현하고 닫았다", "missing-verification": "shared session store, token 암호화, logout, per-route 인가는 계약 밖이고 preference 는 process-global AtomicReference 하나다", "relations": [ "case:split-custody-access-token", "concept:cookie-auth-csrf", "reference:bff-authentication-design-criteria", "decision:bff-owns-token-when-browser-must-not", "question:bff-session-authorized-client-store" ], "kind": "case", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md", "status": "게시 중", "studioId": "d85bd6af-7599-4ef7-9407-6609927d5b5c", "assets": [ "ap3-bff-custody-82fa18bd", "ap3-csrf-split-501dd1f7" ], "evidenceFiles": [] }, { "title": "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유", "slug": "identity-header-trust", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#선택의-이유와-지킨-경계-ap4", "final/document.md#결정이-지켜지는지-확인하는-방법-ap4" ], "code": [ "oauth2-proxy 7.15.2", "Nginx auth_request", "/edge/me" ], "classification": "위조 header 를 실제로 보내 Nginx 가 덮어쓰는지 관측했고 세 겹 방어가 성립하는 조건까지 닫았다", "missing-verification": "role propagation, 신규 endpoint 의 공통 강제, state-changing 요청의 CSRF, session renewal, replica 공유, secret rotation 이 남아 있다", "relations": [ "case:bff-session-csrf-responsibility", "concept:forward-auth-and-auth-request", "reference:forward-auth-identity-header-trust", "question:edge-authorization-scope" ], "kind": "case", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/case/case-ap4-identity-header-trust.md", "status": "게시 중", "studioId": "a0e1cc05-92b3-4dac-bce1-513ab8cd862b", "assets": [ "ap4-edge-trust-1cff2399" ], "evidenceFiles": [] } ], "concept": [ { "title": "Authorization Code와 PKCE가 보호하는 구간", "slug": "authorization-code-and-pkce", "readiness": "READY", "source": [ "final/document.md#문제를-어렵게-만든-제약-로그인-흐름과-api-흐름" ], "basis-version": "Keycloak 26.7.0 · oidc-client-ts", "classification": "code 교환 구간이 로그인 구간과 API 구간으로 갈린다는 것을 먼저 알아야 네 Case 의 경계를 읽을 수 있다", "relations": [ "case:spa-browser-credential-boundary", "reference:authorization-code-endpoint-credential-movement" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-authorization-code-and-pkce.md", "status": "게시 전", "studioId": "75c6c657-3e03-47a0-a9d0-5637fce9dd3f", "assets": [], "evidenceFiles": [] }, { "title": "브라우저가 credential을 보관하는 위치와 그 성질", "slug": "browser-credential-storage", "readiness": "READY", "source": [ "final/document.md#문제를-어렵게-만든-제약-브라우저에-없다" ], "basis-version": "Keycloak 26.7.0 · oidc-client-ts · oauth2-proxy 7.15.2", "classification": "「브라우저에 없다」가 무엇이 없다는 뜻인지 구분해야 AP1 과 AP3·AP4 의 차이를 오해하지 않는다", "relations": [ "case:spa-browser-credential-boundary", "reference:oauth-token-application-session-boundary" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-browser-credential-storage.md", "status": "게시 전", "studioId": "bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07", "assets": [], "evidenceFiles": [] }, { "title": "Bearer JWT가 인증된 principal이 되기까지", "slug": "bearer-jwt-validation-chain", "readiness": "READY", "source": [ "final/document.md#선택의-이유와-지킨-경계-ap1" ], "basis-version": "Keycloak 26.7.0 · Spring Security OAuth2 Resource Server", "classification": "보호 자원이 서명만 보지 않고 issuer·audience·시간까지 본다는 것을 알아야 401 판정을 읽을 수 있다", "relations": [ "case:spa-browser-credential-boundary", "case:split-custody-access-token" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-bearer-jwt-validation-chain.md", "status": "게시 전", "studioId": "87000d59-b69f-4010-9481-0b71c8bde32d", "assets": [], "evidenceFiles": [] }, { "title": "Cookie로 인증하는 요청에서 CSRF token이 하는 일", "slug": "cookie-auth-csrf", "readiness": "READY", "source": [ "final/document.md#선택의-이유와-지킨-경계-ap3" ], "basis-version": "Spring Security 6 CSRF · AP3 BFF 구성", "classification": "cookie 가 자동 첨부된다는 성질을 알아야 BFF 가 CSRF 를 떠안는 이유가 설명된다", "relations": [ "case:bff-session-csrf-responsibility", "reference:bff-authentication-design-criteria" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md", "status": "게시 전", "studioId": "5c8f12d5-1ead-469b-8e91-2de69401df48", "assets": [], "evidenceFiles": [] }, { "title": "Forward-Auth와 Nginx auth_request의 동작", "slug": "forward-auth-and-auth-request", "readiness": "READY", "source": [ "final/document.md#선택의-이유와-지킨-경계-ap4" ], "basis-version": "oauth2-proxy 7.15.2 · Nginx auth_request module", "classification": "실제 요청 전에 별도 endpoint 에 허용을 묻는 구조를 알아야 header 신뢰 조건이 왜 인프라 문제인지 보인다", "relations": [ "case:identity-header-trust", "reference:forward-auth-identity-header-trust" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md", "status": "게시 전", "studioId": "a3493786-d3fb-4b01-b1c5-ecb23c3d5497", "assets": [], "evidenceFiles": [] }, { "title": "외부 IdP Brokering의 동작", "slug": "idp-brokering", "readiness": "READY", "source": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "basis-version": "Keycloak 26.7.0 identity brokering", "classification": "Google 이 Keycloak 앞의 upstream 이라는 구조를 알아야 그것이 다섯 번째 패턴이 아닌 이유가 성립한다", "relations": [ "decision:federation-is-not-an-application-pattern", "reference:external-idp-federation-application-boundary" ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-idp-brokering.md", "status": "게시 전", "studioId": "d99fdec9-fe9e-4e0f-a50b-6fb9b9ed5719", "assets": [], "evidenceFiles": [] } ], "reference": [ { "title": "OAuth/OIDC 인증 패턴 선택 기준", "slug": "oauth-oidc-pattern-selection-criteria", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-책임과-데이터", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로" ], "classification": "다섯 항목으로 구조를 비교하고 피해야 할 조건을 먼저 보는 절차라 다음 프로젝트에도 그대로 적용된다", "scope": "OAuth/OIDC 로 브라우저 인증을 붙이는 구조를 고르는 자리", "exceptions": "브라우저가 없는 machine-to-machine 경로는 credential 보관 위치가 문제되지 않아 이 다섯 축이 걸리지 않는다", "relations": [ "case:spa-browser-credential-boundary", "case:identity-header-trust", "reference:oauth-token-application-session-boundary" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-pattern-selection.md", "status": "게시 중", "studioId": "3f886154-1b85-407b-bda4-57d28370e745", "assets": [], "evidenceFiles": [] }, { "title": "Authorization Code Flow의 Endpoint와 Credential 이동 기준", "slug": "authorization-code-endpoint-credential-movement", "readiness": "READY", "source": [ "final/document.md#문제를-어렵게-만든-제약-로그인-흐름과-api-흐름", "final/document.md#선택이-코드와-흐름에-반영되는-방식-추적-규칙" ], "classification": "요청 하나를 입력·변환·다음 홉·출력 네 칸으로 기록하는 방법이라 다른 인증 구조를 읽을 때도 쓴다", "scope": "code 교환이 있는 로그인 흐름을 처음 읽는 자리", "exceptions": "code 를 쓰지 않는 client credentials 나 이미 발급된 token 만 다루는 경로에는 이 이동표가 필요 없다", "relations": [ "concept:authorization-code-and-pkce", "reference:public-confidential-client-boundary" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md", "status": "게시 중", "studioId": "39fdf472-82c4-43ed-abec-73de672f08ae", "assets": [], "evidenceFiles": [] }, { "title": "Public Client와 Confidential Client 구분 기준", "slug": "public-confidential-client-boundary", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-책임과-데이터", "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#선택의-이유와-지킨-경계-ap2" ], "classification": "secret 을 숨길 수 있는지로 client 종류가 갈리고 그것이 PKCE 요구와 이어진다", "scope": "OAuth client 를 새로 등록하는 자리", "exceptions": "server 끼리만 부르는 client 는 브라우저 노출 축이 없어 이 구분의 비용 계산이 달라진다", "relations": [ "concept:authorization-code-and-pkce", "case:spa-browser-credential-boundary" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-public-confidential-client.md", "status": "게시 중", "studioId": "ede6b9ce-eeed-40c8-9175-9e8116029395", "assets": [], "evidenceFiles": [] }, { "title": "OAuth Token과 Application Session을 구분하는 기준", "slug": "oauth-token-application-session-boundary", "readiness": "READY", "source": [ "final/document.md#문제를-어렵게-만든-제약-같은-사용자를-나타내도", "final/document.md#문제를-어렵게-만든-제약-브라우저에-없다" ], "classification": "같은 사용자 이름이라도 JWT claim 과 edge header 는 검증 방식이 달라 하나로 묶으면 안 된다", "scope": "인증 상태를 저장하거나 전달하는 모든 경계", "exceptions": "IdP 의 SSO 상태는 애플리케이션 credential 이 아니므로 이 구분의 대상이 아니다", "relations": [ "concept:browser-credential-storage", "case:split-custody-access-token" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-token-vs-session.md", "status": "게시 중", "studioId": "66c18e42-116c-459f-86bd-b7e4bf394866", "assets": [], "evidenceFiles": [] }, { "title": "BFF 인증 구조 설계 기준", "slug": "bff-authentication-design-criteria", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "classification": "브라우저에서 토큰을 없앨 때 서버가 새로 떠안는 항목의 목록이라 다음 BFF 에도 적용된다", "scope": "브라우저에 OAuth token 을 두지 않기로 한 구조", "exceptions": "stateless direct API 와 독립 client 가 핵심 요구면 BFF 를 두지 않는 편이 맞다", "relations": [ "case:bff-session-csrf-responsibility", "concept:cookie-auth-csrf", "decision:bff-owns-token-when-browser-must-not" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-bff-auth-design.md", "status": "게시 중", "studioId": "97eddd97-1096-426a-a2c6-a6c5bf1cd09f", "assets": [], "evidenceFiles": [] }, { "title": "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건", "slug": "forward-auth-identity-header-trust", "readiness": "READY", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#선택의-이유와-지킨-경계-ap4", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4" ], "classification": "header 를 인증 근거로 쓰려면 네트워크·덮어쓰기·내부 자격 증명이 서로 독립된 방어선이어야 한다", "scope": "edge 가 인증을 대신하고 upstream 이 header 를 믿는 구조", "exceptions": "backend 로 직접 들어오는 경로를 막을 수 없으면 이 구조 자체를 고르지 않는다", "relations": [ "case:identity-header-trust", "concept:forward-auth-and-auth-request" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-forward-auth-header-trust.md", "status": "게시 중", "studioId": "004dd0a2-5fb3-4f25-80c9-576f709de331", "assets": [], "evidenceFiles": [] }, { "title": "외부 IdP 연동과 Application 인증 구조의 경계", "slug": "external-idp-federation-application-boundary", "readiness": "READY", "source": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "classification": "upstream IdP 검증 범위와 application credential 경계를 분리하는 기준이라 다른 federation 에도 적용된다", "scope": "Keycloak 같은 broker 앞에 외부 IdP 를 두는 구성", "exceptions": "애플리케이션이 외부 IdP 와 직접 OIDC 를 맺으면 broker 경계가 없어 이 분리가 성립하지 않는다", "relations": [ "concept:idp-brokering", "decision:federation-is-not-an-application-pattern" ], "kind": "reference", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/reference/reference-idp-federation-boundary.md", "status": "게시 중", "studioId": "1a00a640-8987-4075-a9e4-7ec023cdffbb", "assets": [], "evidenceFiles": [] }, { "title": "패턴 검증을 실제로 돌릴 때의 안전한 순서", "slug": "runtime-verification-safe-order", "readiness": "READY", "source": [ "final/document.md#결정이-지켜지는지-확인하는-방법-실제-runtime-검증을-수행할-때의-안전한-순서" ], "classification": "사전 조건·순서·중단 조건이 갖춰진 운영 절차이고 다른 프로젝트의 파괴적 검증에도 그대로 적용된다", "scope": "volume 을 지우고 스택을 다시 세우는 검증 절차를 돌리는 자리", "exceptions": "일회용 환경이 아닌 곳에서는 이 절차를 그대로 실행하지 않고 별도 project 로 복제한 뒤 돌린다", "relations": [ "case:identity-header-trust", "reference:oauth-oidc-pattern-selection-criteria" ], "kind": "reference", "publication": "초안", "file": "oauth-oidc-auth-boundary/reference/reference-runtime-verification-order.md", "status": "게시 전", "studioId": "", "assets": [], "evidenceFiles": [] } ], "question": [ { "title": "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가", "slug": "bff-session-authorized-client-store", "readiness": "OPEN", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#문제를-어렵게-만든-제약-학습-환경", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "known": "현재 구현은 단일 인스턴스 memory 만 사용한다. 재시작이나 replica 이동 뒤 로그인 유지는 확인하지 못했다", "unknown": "세션과 authorized client 를 어느 저장소에 둘 것인가, 저장 token 암호화와 key 교체를 어떻게 할 것인가", "next-verification": "shared store 를 붙인 뒤 재시작과 replica 이동에서 로그인이 유지되는지 확인한다", "decision-criterion": "재시작과 replica 이동 양쪽에서 로그인이 유지되면 그 저장소를 채택하는 Decision 으로 넘긴다", "relations": [ "case:bff-session-csrf-responsibility", "reference:bff-authentication-design-criteria", "decision:bff-owns-token-when-browser-must-not" ], "kind": "question", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/question/question-bff-state-store.md", "status": "게시 중", "studioId": "18a5cde2-dd1e-4bff-9f1c-997577ae438f", "assets": [], "evidenceFiles": [] }, { "title": "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가", "slug": "server-session-pattern-multi-instance", "readiness": "OPEN", "source": [ "final/document.md#문제를-어렵게-만든-제약-학습-환경", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap2" ], "known": "AP2 와 AP3 가 session 과 authorized client 를 여러 인스턴스에서 공유하는지 확인하지 못했다", "unknown": "session affinity 로 갈 것인가 shared store 로 갈 것인가, 장애 복구 시간은 얼마인가", "next-verification": "두 인스턴스를 띄우고 요청을 번갈아 보내며 로그인 상태가 유지되는지 본다", "decision-criterion": "한쪽 방식이 재인증 없이 유지되면 그것을 채택하고, 둘 다 실패하면 세션 모델 자체를 다시 정한다", "relations": [ "case:split-custody-access-token", "question:bff-session-authorized-client-store" ], "kind": "question", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/question/question-multi-instance-session.md", "status": "게시 중", "studioId": "c72656b5-842d-45d9-b5f6-82b66b09d0b9", "assets": [], "evidenceFiles": [] }, { "title": "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가", "slug": "refresh-rotation-replica-contention", "readiness": "OPEN", "source": [ "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap2" ], "known": "AP1 은 rotation 과 reuse 0 을 쓴다. 이미 발급된 access token 은 만료 전까지 유효하다", "unknown": "여러 replica 가 같은 refresh token 을 동시에 쓰면 무엇이 먼저 거부되는가, 재인증을 어떻게 이어 갈 것인가", "next-verification": "두 replica 에서 동시에 refresh 를 시도해 어느 쪽이 거부되고 사용자에게 무엇이 보이는지 관측한다", "decision-criterion": "한쪽만 거부되고 재인증으로 복구되면 닫고, 사용자 세션이 끊기면 rotation 정책을 다시 정하는 Decision 으로 넘긴다", "relations": [ "case:split-custody-access-token", "question:server-session-pattern-multi-instance" ], "kind": "question", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/question/question-refresh-rotation-replica.md", "status": "게시 중", "studioId": "9ae4ec71-a32e-49a7-88c2-f7368541c28d", "assets": [], "evidenceFiles": [] }, { "title": "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가", "slug": "edge-authorization-scope", "readiness": "OPEN", "source": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4" ], "known": "현재는 user 와 email 만 전달하고 internal token 확인은 controller 하나에만 있다. /edge/** 전체를 filter 가 보호하지는 않는다", "unknown": "role 과 claim 을 어디까지 header 로 투영할 것인가, 공통 강제를 filter 로 옮길 것인가 policy service 로 뺄 것인가", "next-verification": "endpoint 를 하나 더 추가해 같은 검사가 자동으로 걸리는지 확인한다", "decision-criterion": "새 endpoint 가 검사 없이 통과하면 공통 filter 로 옮기는 Decision 으로 넘긴다", "relations": [ "case:identity-header-trust", "reference:forward-auth-identity-header-trust" ], "kind": "question", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/question/question-edge-authorization-scope.md", "status": "게시 중", "studioId": "7ff40767-a00b-4db2-98f6-0cdfce8c8936", "assets": [], "evidenceFiles": [] } ], "decision": [ { "title": "BFF가 OAuth Token을 관리하는 조건", "slug": "bff-owns-token-when-browser-must-not", "readiness": "READY", "decision-status": "PROPOSED", "source": [ "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "decision-evidence": [ "case:bff-session-csrf-responsibility", "reference:bff-authentication-design-criteria" ], "grounds": "브라우저에서 OAuth token 을 없애려면 서버가 code 교환뿐 아니라 API 호출까지 맡아야 하고, 그 순간 session·CSRF·저장소가 함께 따라온다", "classification": "AP1·AP2 를 대안으로 두고 브라우저 토큰 금지가 요구일 때만 BFF 를 고르기로 정했다", "relations": [ "case:bff-session-csrf-responsibility", "question:bff-session-authorized-client-store" ], "kind": "decision", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/decision/decision-bff-owns-token.md", "status": "게시 중", "studioId": "19b55c39-c583-4161-9775-df954280a568", "assets": [], "evidenceFiles": [] }, { "title": "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다.", "slug": "federation-is-not-an-application-pattern", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "decision-evidence": [ "concept:idp-brokering", "reference:external-idp-federation-application-boundary" ], "grounds": "Google 은 Keycloak 앞의 upstream 이고 애플리케이션으로 나가는 데이터는 다시 Keycloak 이 만든다. 네 패턴의 downstream 경계는 그대로다", "classification": "federation 을 다섯 번째 패턴으로 세는 대안을 두고 두 protocol boundary 를 섞지 않기로 정했다", "relations": [ "concept:idp-brokering", "reference:external-idp-federation-application-boundary" ], "kind": "decision", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/decision/decision-federation-not-a-pattern.md", "status": "게시 중", "studioId": "8c1ebea7-204e-445c-9812-0421d9eb0e9c", "assets": [], "evidenceFiles": [] } ] } } }, "candidates": [ { "id": "SSOT-spa-browser-credential-boundary", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap1", "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#결정이-지켜지는지-확인하는-방법-ap1" ], "summary": "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:spa-browser-credential-boundary", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-split-custody-access-token", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap2", "final/document.md#선택의-이유와-지킨-경계-ap2", "final/document.md#결정이-지켜지는지-확인하는-방법-ap2" ], "summary": "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:split-custody-access-token", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-bff-session-csrf-responsibility", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#결정이-지켜지는지-확인하는-방법-ap3" ], "summary": "BFF에서 Browser Token을 제거하고 Session과 CSRF를 처리한 방식", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:bff-session-csrf-responsibility", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-identity-header-trust", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#선택의-이유와-지킨-경계-ap4", "final/document.md#결정이-지켜지는지-확인하는-방법-ap4" ], "summary": "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:identity-header-trust", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-authorization-code-and-pkce", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-로그인-흐름과-api-흐름" ], "summary": "Authorization Code와 PKCE가 보호하는 구간", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:authorization-code-and-pkce", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-browser-credential-storage", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-브라우저에-없다" ], "summary": "브라우저가 credential을 보관하는 위치와 그 성질", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:browser-credential-storage", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-bearer-jwt-validation-chain", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#선택의-이유와-지킨-경계-ap1" ], "summary": "Bearer JWT가 인증된 principal이 되기까지", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:bearer-jwt-validation-chain", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-cookie-auth-csrf", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#선택의-이유와-지킨-경계-ap3" ], "summary": "Cookie로 인증하는 요청에서 CSRF token이 하는 일", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:cookie-auth-csrf", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-forward-auth-and-auth-request", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#선택의-이유와-지킨-경계-ap4" ], "summary": "Forward-Auth와 Nginx auth_request의 동작", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:forward-auth-and-auth-request", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-idp-brokering", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "summary": "외부 IdP Brokering의 동작", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "concept:idp-brokering", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-oauth-oidc-pattern-selection-criteria", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-책임과-데이터", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로" ], "summary": "OAuth/OIDC 인증 패턴 선택 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:oauth-oidc-pattern-selection-criteria", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-authorization-code-endpoint-credential-movement", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-로그인-흐름과-api-흐름", "final/document.md#선택이-코드와-흐름에-반영되는-방식-추적-규칙" ], "summary": "Authorization Code Flow의 Endpoint와 Credential 이동 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:authorization-code-endpoint-credential-movement", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-public-confidential-client-boundary", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-책임과-데이터", "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#선택의-이유와-지킨-경계-ap2" ], "summary": "Public Client와 Confidential Client 구분 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:public-confidential-client-boundary", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-oauth-token-application-session-boundary", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-같은-사용자를-나타내도", "final/document.md#문제를-어렵게-만든-제약-브라우저에-없다" ], "summary": "OAuth Token과 Application Session을 구분하는 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:oauth-token-application-session-boundary", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-bff-authentication-design-criteria", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "summary": "BFF 인증 구조 설계 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:bff-authentication-design-criteria", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-forward-auth-identity-header-trust", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#선택의-이유와-지킨-경계-ap4", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4" ], "summary": "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:forward-auth-identity-header-trust", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-external-idp-federation-application-boundary", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "summary": "외부 IdP 연동과 Application 인증 구조의 경계", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:external-idp-federation-application-boundary", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-runtime-verification-safe-order", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#결정이-지켜지는지-확인하는-방법-실제-runtime-검증을-수행할-때의-안전한-순서" ], "summary": "패턴 검증을 실제로 돌릴 때의 안전한 순서", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:runtime-verification-safe-order", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-bff-session-authorized-client-store", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap3", "final/document.md#문제를-어렵게-만든-제약-학습-환경", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "summary": "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:bff-session-authorized-client-store", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-server-session-pattern-multi-instance", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-학습-환경", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap2" ], "summary": "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:server-session-pattern-multi-instance", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-refresh-rotation-replica-contention", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#선택의-이유와-지킨-경계-ap1", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap2" ], "summary": "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:refresh-rotation-replica-contention", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-edge-authorization-scope", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#검토한-선택지와-막힌-지점-ap4", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap4" ], "summary": "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:edge-authorization-scope", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-bff-owns-token-when-browser-must-not", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#선택의-이유와-지킨-경계-ap3", "final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3" ], "summary": "BFF가 OAuth Token을 관리하는 조건", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:bff-owns-token-when-browser-must-not", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-federation-is-not-an-application-pattern", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login" ], "summary": "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다.", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:federation-is-not-an-application-pattern", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-learning-environment-scope", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#문제를-어렵게-만든-제약-학습-환경" ], "summary": "현재 구현은 운영 참조 아키텍처가 아니라 관찰 가능한 학습 환경이다", "disposition": "KEEP_IN_SSOT", "dispositionReview": "CONFIRMED", "target": null, "reason": "이 분석 전체의 범위 고지다. 각 Case 의 확인하지 못한 것과 네 Question 이 그 한계를 이미 나눠 담는다" }, { "id": "SSOT-request-trace-four-columns", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#선택이-코드와-흐름에-반영되는-방식-추적-규칙" ], "summary": "요청 한 번을 입력·변환·다음 홉·출력 네 칸으로 기록한다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:authorization-code-endpoint-credential-movement", "reason": "그 Reference 가 이미 이 네 칸으로 credential 이동을 적는다. 별도 규칙으로 빼면 같은 말이 둘이 된다" }, { "id": "SSOT-acceptance-contract-table", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#결정이-지켜지는지-확인하는-방법-테스트-개수보다-경계의-input과-output" ], "summary": "패턴별 테스트가 선언하는 acceptance contract 표", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "case:identity-header-trust", "reason": "패턴마다의 입력과 기대 출력은 각 Case 의 재현 조건과 확인하지 못한 것이 이미 나눠 담는다" }, { "id": "SSOT-four-patterns-not-a-ladder", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#얻은-것-잃은-것-적용하지-않을-때-사다리가-아니라" ], "summary": "네 패턴은 사다리가 아니라 서로 다른 운영 계약이다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:oauth-oidc-pattern-selection-criteria", "reason": "번호로 판단하지 않는다는 것이 그 Reference 의 전제이고 규칙 1·4 가 이미 그 형태로 적는다" }, { "id": "SSOT-credential-contract-migration", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#얻은-것-잃은-것-적용하지-않을-때-변경-경로" ], "summary": "패턴을 옮기는 일도 credential contract 의 변화로 본다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:oauth-oidc-pattern-selection-criteria", "reason": "그 Reference 의 규칙 5 「자격 증명의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다」가 같은 내용이다" } ], "unlisted": [], "history": {}, "counts": { "topics": 1, "nodes": 24, "written": 24, "unwritten": 0, "unlisted": 0, "candidates": 29 } }