30 KiB
title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
| title | source_type | status | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | branch | parent_branch | related_projects | tags | created | target_merge | status_label | contract_packet_sha256 | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-keycloak-spa-token-storage-tradeoff (Token 저장 위치 trade-off — localStorage / sessionStorage / memory / httpOnly cookie) | branch-note | raw | BR-KEYCLOAK-PATTERNS-OVERVIEW-006 | project-work-item | keycloak-patterns-overview | WI-KEYCLOAK-PATTERNS-OVERVIEW-006 |
|
|
1 | feature-keycloak-spa-token-storage-tradeoff |
|
|
2026-05-25 | in-progress | 92acc553e2b9cf25e7fb7a8d9030574d20891bef35e5e652038e480f067be1c3 |
branch: feature-keycloak-spa-token-storage-tradeoff — Token 저장 위치 trade-off
Layer:
raw/branch-notes/— raw/project-notes/keycloak-patterns-overview의WI-KEYCLOAK-PATTERNS-OVERVIEW-006직접 branch. 목적: access_token / refresh_token을 SPA에서 어디에 저장할지 결정하기 위한 4 저장소(localStorage / sessionStorage / memory / httpOnly cookie)의 XSS·CSRF 노출 trade-off를 표로 정리.status_label:in-progress|review|merged|abandoned
부모 (필수)
raw/project-notes/keycloak-patterns-overview
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: 저장 위치별 browser token read/XSS surface가 재현되고 선택이 기록된다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1 |
AP1은 public client와 Authorization Code + PKCE를 사용한다 | public SPA가 보유하는 token의 저장 위치와 XSS surface 비교에 적용한다 | raw/project-notes/keycloak-patterns-overview |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
없음.
목표
OWASP HTML5 Storage cheat sheet: "Do not store sensitive data in Web Storage." — localStorage / sessionStorage는 동일 origin의 모든 JS가 접근 가능 → XSS 1회 발생 시 토큰 즉시 탈취. 반면 httpOnly cookie는 JS 접근 불가지만 CSRF surface가 생긴다. 두 surface 중 무엇을 선택해 무엇을 방어할지 의식적으로 결정해야 한다.
핵심 질문:
- 4 저장소(localStorage / sessionStorage / memory / httpOnly cookie)의 XSS 노출과 CSRF 노출 비교?
- refresh_token은 왜 access_token보다 더 엄격히 보호해야 하는가? (긴 TTL × 새 access_token 발급 권한)
- OAuth 2.1 draft가 refresh_token 저장에 대해 권고하는 것은?
- SPA reload 시 silent refresh / refresh_token cookie 패턴의 장단점?
- Silent renew(iframe +
prompt=none)는 왜 3rd-party cookie 제약으로 점점 어려워지는가?
본 sub-sub-branch는 저장소별 비교표 + 권장 조합 + reload UX 고려를 정리한다.
- 이슈: (학습 노트, 이슈 없음)
- PR: (구현 없음)
범위
포함 범위
- 4 저장소(localStorage / sessionStorage / memory / httpOnly cookie)의 XSS·CSRF 노출 비교표 (문서화,
documented-only) - pure SPA의 access_token / refresh_token memory-only baseline과 reload 재인증 결정
- SPA reload 시 access_token 재획득 흐름(silent refresh / refresh_token grant / 재로그인) 옵션 비교
- TMB/BFF variant를 별도 채택할 때 필요한 cookie/CSRF 계약의 경계 명시
- PKCE
code_verifier저장 위치 결정 (D6)
제외 범위
의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
- 실제 SPA 구현 — vanilla JS SPA 의 token 메모리 보관/
/refresh호출 구현은 raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce - BFF 백엔드 구현 — 본 branch 는 SPA Direct 전제. BFF vs SPA Direct 결정 자체는 raw/branch-notes/feature-keycloak-bff-vs-spa-direct
- refresh_token rotation / revocation 메커니즘 상세 — raw/branch-notes/feature-keycloak-refresh-token-rotation (본 branch 는 "rotation 에 의존"만 결정, 메커니즘은 consume)
- CSRF token 발급/검증의 backend 실 구현 — pure SPA baseline에는 cookie credential이 없어 부과하지 않는다. HttpOnly refresh cookie를 쓰는 TMB/BFF variant를 채택하면 endpoint·cookie lifecycle·CSRF negative test를 소유하는 별도 계약을 먼저 지정해야 한다(
OWNER_REQUIRED; audience-validator로 위임하지 않음) - Keycloak realm/client 설정 상세 — raw/branch-notes/feature-keycloak-realm-client-export
근거 (필수, 최소 1개+)
- raw/official-docs/owasp-html5-storage-xss-spa — OWASP HTML5 Storage cheat sheet + XSS in SPA
- raw/official-docs/oauth-v2-1-draft-ietf — OAuth 2.1 (refresh_token rotation / sender-constrained)
- raw/company-tech-blogs/curity-bff-pattern-spa — Curity: 토큰을 브라우저에서 분리하라는 권고
- raw/official-docs/third-party-cookie-blocking-safari-webkit-official — WebKit: Safari 13.1 / iOS 13.4 (2020-03-24) 이후 third-party cookie 기본 차단 → D3 (silent renew
prompt=none실패) 근거 - raw/official-docs/silent-check-sso-third-party-cookies-keycloak-official — Keycloak JS adapter 공식 문서: silent check-sso의 hidden iframe 메커니즘 + third-party cookie 의존 + Safari 13.1+ fallback (D3 MECHANISM 근거)
- raw/official-docs/chrome-third-party-cookie-policy-google-official — Google Privacy Sandbox (2025-04-22): Chrome은 3rd-party cookie 기본 차단 계획 철회(no new standalone prompt), Incognito만 기본 차단 → D3a (Chrome 일반 모드 silent renew 현재 동작) 근거 + "Chrome phase-out" 통념 정정
- raw/official-docs/oauth2-pkce-rfc-7636 — RFC 7636:
code_verifier= per-request 생성·기록 secret (D6 verifier lifetime 근거) - raw/official-docs/oauth2-browser-based-apps-ietf-draft — OAuth 2.0 for Browser-Based Apps BCP: §8 은 access/refresh token 저장만 다루고
code_verifier는 0회 언급 → D6 의 "sessionStorage 는 BCP 직접 권고 아님(INFERENCE)" 근거
TODO
각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation
- 4 저장소 노출 비교표 — 등급:
documented-only저장소 JS 접근 XSS 노출 CSRF 노출 reload 후 유지 적합 토큰 localStorage✅ 높음 (모든 JS) 낮음 (자동 첨부 안 됨) ✅ 영구 권장 안 함 sessionStorage✅ 높음 (탭별, 모든 JS) 낮음 ✅ 탭 내 (권장 안 함, 단 PKCE verifier는 가능) 메모리 (JS 변수) ✅ 낮음 (런타임만, debugger 접근 가능하나 영속 X) 낮음 ❌ 잃음 pure SPA access/refresh baseline httpOnly secure cookie ❌ 낮음 (JS 접근 불가) 높음 (자동 첨부) → SameSite+ CSRF 방어 필요✅ cookie TTL TMB/BFF variant only - refresh_token 저장 권고 — 등급:
documented-only- pure SPA baseline: 메모리 only. reload 시 재인증
- TMB/BFF variant: httpOnly + Secure cookie. server-side endpoint와 CSRF 계약을 함께 소유할 때만
- 절대 금지: localStorage / sessionStorage (RFC 6749 §10.4 refresh_token confidentiality)
- access_token 저장 권고 — 등급:
documented-only- 권장: 메모리 (JS 변수 / closure) — reload 시 silent refresh로 재취득
- TTL: 5~15분 (짧을수록 탈취 시 피해 감소)
- OAuth 2.1 draft 인용 — 등급:
documented-only- "Refresh tokens MUST be sender-constrained or use refresh token rotation."
- SPA 환경에서는 sender-constrained(mTLS / DPoP) 어렵 → rotation 의존
- SPA reload 시 흐름 옵션 — 등급:
documented-only- baseline: memory 소실 → 사용자 재인증
- 대안: silent SSO는 별도 브라우저/배포 조건 검증
- variant: httpOnly refresh cookie +
/refresh는 TMB/BFF로 분류
- Silent renew 함정 — 등급:
documented-only- 1st-party context: Keycloak이 same-site면 동작
- 3rd-party context: Safari ITP / Chrome 3rd-party cookie phase-out → Keycloak SSO cookie를 iframe에서 못 읽음 → silent renew 실패
- 정정 (2026-07-18, → D3/D3a): "Chrome 3rd-party cookie phase-out" 은 부정확 — Google 2025-04-22 발표로 Chrome 일반 모드는 기본 미차단(Incognito 만 차단). cross-site 기본 차단이 확정된 것은 Safari(ITP) 뿐. Decision Evidence Map D3(Safari) + D3a(Chrome) 참조. raw/official-docs/chrome-third-party-cookie-policy-google-official
- 대안: refresh_token grant 직접 사용 (cookie 또는 메모리)
- XSS 발생 시 시나리오 — 등급:
documented-only- localStorage: 즉시 토큰 탈취 + 영속 (브라우저 종료 후에도)
- 메모리: 현재 페이지 세션 내 탈취 (이후 fetch 후킹은 가능하나 영속 X)
- httpOnly cookie: JS 접근 불가지만
fetch(/api, {credentials: 'include'})로 공격자가 SPA 도메인 내에서 API 호출은 가능 → CSRF 토큰으로 추가 방어
- CSRF 방어 (cookie 사용 시) — 등급:
documented-onlySameSite=Strict(cross-site 자동 첨부 차단)-
- double-submit CSRF token (header X-CSRF-Token)
-
- Origin / Referer 검증
진행 중 메모
작업하며 떠오른 메모. 자유 형식.
- "메모리 저장은 안전하다"는 단순 명제는 아님 — XSS 페이로드가 fetch wrapper를 후킹하면 메모리에 있어도 모든 요청이 가로채짐. 단 영속성은 없음 (reload 시 사라짐).
- BFF 패턴이 사실상 가장 깔끔한 해법이지만 백엔드 stateful + session 공유 필요 → P2A 본 branch에서는 SPA Direct를 채택했음.
- PKCE
code_verifier는 매우 단명(seconds) → sessionStorage도 허용 가능 (단 메모리가 더 안전).
결정 사항 (decisions)
추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록.
- 2026-05-25 (historical, superseded):
P2A 권장 조합 = access_token 메모리 + refresh_token secure HttpOnly cookie. - 2026-07-18: pure SPA baseline = access/refresh token 모두 memory-only, reload 시 재인증. HttpOnly refresh cookie는 최소 TMB/BFF variant이며 AP1 baseline에 포함하지 않는다.
- 2026-07-18: TMB/BFF variant를 채택할 때만 별도 cookie/CSRF owner를 지정한다. 현재 branch set에는 그 구현 owner가 없으므로
OWNER_REQUIRED로 남긴다. - 2026-05-25: localStorage 사용은 모든 토큰에 대해 금지로 기록 (OWASP).
- 2026-05-25: silent renew는 3rd-party cookie 제약으로 long-term 권장 안 함 → refresh_token grant 직접 사용 우선.
- 2026-07-18 (
/branch-spec자동조사 보강): D3 를 브라우저·토폴로지 조건부로 정밀화. (1) Keycloak same-site 면 silent renew 동작 / cross-site + Safari 는 ITP 로 구조적 실패(어댑터가 full-redirect fallback) → refresh_token grant 우선. (2) 정정 — "Chrome 3rd-party cookie phase-out" 전제는 부정확: Google 2025-04-22 발표로 Chrome 일반 모드는 기본 미차단(Incognito 만 차단) → 신규 D3a 로 분리. 근거: raw/official-docs/third-party-cookie-blocking-safari-webkit-official, raw/official-docs/silent-check-sso-third-party-cookies-keycloak-official, raw/official-docs/chrome-third-party-cookie-policy-google-official. - 2026-07-18 (
/branch-spec자동조사 보강): D6 를UNSUPPORTED에서 해소 — full-page redirect 전제에서 PKCEcode_verifier저장 = sessionStorage (in-memory 는 redirect 생존 불가, localStorage 는 OWASP 반대). 단 "BCP 직접 권고 아님(INFERENCE)" 명시 — Browser-Based Apps BCP 는code_verifier를 언급하지 않음. 근거:OWASP-HTML5-C4+PKCE-RFC7636-C2.
결정-근거 매핑
각 결정의 직접 근거.
선택 조건열(R2): 이 조건일 때 이 결정 / 다른 조건이면 어떤 대안. Strength 어휘: OWASP cheatsheet =official-reference, OAuth 2.1 / RFC 7636 =official-standard, WebKit / Chrome / Keycloak vendor doc =official-vendor-doc, Curity blog =company-case-study. company-tech-blog 단독으로 "공식 best practice" 단언 금지. D6 의 저장 위치 권고는INFERENCE— BCP 직접 문장이 아니라 OWASP 원칙 + verifier lifetime + 실무 관행의 사슬(하단 Open Risk 참조).
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | pure SPA baseline = access/refresh token 모두 memory-only, reload 시 재인증 | server-side token custody가 없는 AP1이면 이 결정. 세션 지속이 필수면 D7의 TMB/BFF variant로 패턴을 바꾼다 | raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C1, #OWASP-HTML5-C2, #OWASP-HTML5-C3 |
official-reference + project architecture decision |
memory token도 실행 중 XSS에 노출된다. 이 선택은 persistence를 제거할 뿐 XSS 자체를 제거하지 않음 |
| D2 | localStorage 사용은 모든 토큰에 대해 금지 | N/A (무조건) — XSS 위협 모델을 가정하는 모든 SPA. XSS 를 위협 모델에서 완전 배제 가능하면 예외 후보이나 OWASP 는 그 가정 불허 | raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C1, raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C2, raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C3 |
official-reference |
sessionStorage 도 동일 위협 (OWASP-HTML5-C4 Does not prove: "sessionStorage 가 XSS 에 안전하다는 뜻은 아님 — C2/C3 는 these objects 즉 둘 다에 적용"). 본 branch 본문 표의 "sessionStorage XSS 노출 높음" 은 정합 |
| D3 | Keycloak hidden-iframe silent renew(prompt=none)는 Keycloak cross-site + Safari 에서 구조적으로 실패 → refresh_token grant 직접 사용(rotation 의존) 우선 |
Keycloak same-site(SPA 와 동일 registrable domain) → silent renew 동작(유지 가능). Keycloak cross-site + Safari(ITP) → 실패(어댑터가 full-redirect fallback → "silent" 상실) → refresh_token grant. Chrome cross-site 는 D3a 참조 | raw/official-docs/silent-check-sso-third-party-cookies-keycloak-official.md#KC-JSADAPTER-C1, ...#KC-JSADAPTER-C2, ...#KC-JSADAPTER-C3, ...#KC-JSADAPTER-C4, ...#KC-JSADAPTER-C5, raw/official-docs/third-party-cookie-blocking-safari-webkit-official.md#WEBKIT-3PC-C1, ...#WEBKIT-3PC-C3 |
official-vendor-doc (Keycloak + WebKit) |
same-site vs cross-site 판정 단위(eTLD+1)의 직접 인용은 본 Sources 에 미확보(WebKit 블로그에 없음 → webkit.org/tracking-prevention 별도 아카이빙 필요, Should-fix). "silent renew 는 항상 안 된다" 는 과장 — same-site 배포면 동작. refresh_token grant 의 저장 위치 문제는 D1 · §엣지 참조 |
| D3a | Chrome 은 (2026-07-18 조사 시점 stated policy) 일반 모드에서 3rd-party cookie 기본 미차단(Incognito 만 차단) → "Chrome 3rd-party cookie phase-out" 통념은 부정확 | Chrome 일반 모드 + cross-site → silent renew 현재 동작(단 정책 불안정). Chrome Incognito → 차단 → 실패. 사용자가 수동 3PC off → 브라우저 무관 실패 | raw/official-docs/chrome-third-party-cookie-policy-google-official.md#CHROME-3PC-C1, ...#CHROME-3PC-C3 |
official-vendor-doc |
Google 정책은 2020~2025 수차례 번복(2025-04-22 철회) → "확정적 장기 사실" 인용 금지, "조사 시점 stated policy" 로만. 장기 아키텍처를 현재 Chrome 정책에 고정하는 것 비권장. (skycloak.io 등 "Chrome deprecation 중" 주장은 이 공식 vendor 소스와 상충 → 채택 안 함) |
| D4 | refresh_token 은 rotation 에 의존 (sender-constrained mTLS/DPoP 어려움) | SPA(public client)라 sender-constrained(mTLS/DPoP) 어려움 → rotation. mTLS/DPoP 지원 환경(confidential client 전환 등)이면 sender-constrained 상위. rotation 메커니즘·재사용탐지는 sibling raw/branch-notes/feature-keycloak-refresh-token-rotation 위임 | raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3 (refresh token 은 scope + resource server 에 bound MUST), raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C4 (BFF 권고) |
official-standard |
본문 "Refresh tokens MUST be sender-constrained or use refresh token rotation" 의 직접 verbatim 은 본 branch Sources 의 OAuth 2.1 발췌(OA21-C1~C6)에 미포함 — sibling refresh-token-rotation branch 가 rotation 상세를 owns. 현 D4 는 OA21-C3(bound) + OA21-C4(BFF)로 부분 corroborate |
| D5 | refresh token 탈취 시 유효 기간 동안 victim 데이터 접근 가능 — SPA Direct 의 핵심 위험 | N/A (위험 진술). 이 위험 감수 불가 → BFF 전환(refresh_token 을 브라우저에서 제거, D1 대안) | raw/company-tech-blogs/curity-bff-pattern-spa.md#CURITY-BFF-C6, raw/company-tech-blogs/curity-bff-pattern-spa.md#CURITY-BFF-C2 |
company-case-study |
Curity vendor 권고. 공식 표준 측 corroborate 는 OA21-C3(refresh token binding MUST)와 결합. rotation mitigation 효과는 본 인용 미포함(sibling 위임) |
| D6 | PKCE code_verifier 저장 = sessionStorage (full-page redirect 전제) — in-memory 는 redirect 생존 불가, localStorage 는 OWASP 의 "persistence 불필요 시 sessionStorage" 조건에 반함 |
full-page redirect flow(탭 전체 navigate) → 메모리 verifier 파괴 → sessionStorage. popup/iframe 로 부모 탭 메모리 유지 가능하면 in-memory 가 더 안전. multi-tab 로그인 UX 요구 → cookie transaction(Auth0 useCookiesForTransaction) 별도 검토(N=3 범위 밖) |
raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C4 (persistence 불필요 시 sessionStorage), raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C2 (verifier = 생성·기록 per-transaction secret) |
official-reference(OWASP) + official-standard(RFC 7636) + INFERENCE(저장 위치) |
"sessionStorage 가 BCP 권고" 표현 금지 — OAuth 2.0 for Browser-Based Apps BCP 는 code_verifier 를 0회 언급(§8 은 token 전용, oauth2-browser-based-apps-ietf-draft 확인). 저장 위치 결정은 OWASP 일반 원칙 + verifier lifetime + 실무 관행의 inference 사슬이지 단일 official 직접 인용 아님. verifier(sessionStorage) 탈취는 authorization code 없이 무가치 → token 탈취보다 심각도 낮음 |
| D7 | HttpOnly refresh cookie는 TMB/BFF variant에서만 허용 | reload 없는 세션 지속이 memory-only UX보다 중요하고, server-side /refresh·cookie lifecycle·CSRF negative test owner를 함께 둘 때만. 그 계약이 없으면 D1 유지 |
raw/company-tech-blogs/curity-bff-pattern-spa.md#CURITY-BFF-C1, #CURITY-BFF-C6 |
company-case-study + architecture boundary |
현재 구현 owner 없음(OWNER_REQUIRED). audience-validator는 bearer 검증 owner이지 cookie/CSRF owner가 아님 |
구현 가이드
본 branch 는
documented-only— 여기서의 "구현" 은 다운스트림 구현 branch(raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce)가 소비할 저장 위치 배치 명세다. 각 row 는 Decision ID + Supporting Claim ID 를 Trace 하거나UNSUPPORTED_IMPL_DECISION라벨을 단다(CLAUDE.md §15.5 3-rule).
1. 토큰·secret 저장 위치 배치 명세
Trace: D1 (
OWASP-HTML5-C1/C2/C3,CURITY-BFF-C6), D2 (OWASP-HTML5-C1~C3), D6 (OWASP-HTML5-C4,PKCE-RFC7636-C2)
- UNSUPPORTED_IMPL_DECISION: (1) refresh_token cookie 의
Path범위, (2) verifier sessionStorage 키명 — 아래 표에 개별 명시.
| 대상 | 저장 위치 | 속성 / 키 | Trace | 라벨 |
|---|---|---|---|---|
| access_token | 메모리 (모듈 스코프 closure 변수, non-exported) | reload 시 §2 흐름으로 재취득 | D1 / OWASP-HTML5-C1·C2 |
— |
| refresh_token | 메모리 (access_token과 동일한 in-memory store) | reload 시 폐기하고 재인증 | D1 / OWASP-HTML5-C1·C2 |
— |
| localStorage / sessionStorage | 토큰 저장 금지 | — | D2 / OWASP-HTML5-C1·C2·C3 |
— |
PKCE code_verifier |
sessionStorage | 토큰 교환 성공 즉시 removeItem |
D6 / OWASP-HTML5-C4, PKCE-RFC7636-C2 |
UNSUPPORTED_IMPL_DECISION: 키명(예 kc_pkce_verifier)은 임의 — trade-off: 키에 state 포함(...-${state})하면 multi-tab 동시 로그인 충돌 방지(Auth0 관행), 고정키는 단순하나 탭 충돌 |
HttpOnly refresh cookie는 D7 variant다. backend
/refresh가Set-Cookie하고 CSRF를 검증해야 하므로 pure SPA 배치표에 섞지 않는다.
2. reload 후 access_token 재취득 흐름
Trace: D1, D3 (Safari cross-site: silent renew 실패), D3a (Chrome normal-mode: 현재 미차단이나 정책 불안정)
| 옵션 | 흐름 | 언제 이 옵션 | Trace |
|---|---|---|---|
| (a) 같은 page session의 refresh_token grant | memory refresh_token으로 새 access_token을 받아 둘 다 memory에 갱신 | reload 전 활성 session에서 rotation을 시연할 때 | D1/D4, sibling raw/branch-notes/feature-keycloak-refresh-token-rotation |
(b) silent renew (hidden iframe + prompt=none) |
iframe 에서 Keycloak SSO cookie 로 재발급 | Keycloak same-site(eTLD+1 동일)일 때 안정. Chrome normal cross-site 도 현재 동작하나 정책 불안정 | D3, D3a |
| (c) 메모리 only + 재인증 | reload 시 토큰 소실 → 사용자 재인증 | pure SPA baseline | D1 |
3. TMB/BFF variant의 cookie·CSRF 선행 계약
Trace: D1 (
OWASP-HTML5-C5: cookie 는 path 제한 가능하나 CSRF 는 별도 surface)
- OUT_OF_BRANCH_SCOPE / OWNER_REQUIRED: pure SPA에는 이 계약을 적용하지 않는다. D7 variant를 채택할 때
/refresh,Set-Cookie, logout/revoke, CSRF token 발급·검증과 negative test를 한 별도 owner D-row에 먼저 배정한다. raw/branch-notes/feature-keycloak-spring-rs-audience-validator는 bearer JWT 검증 owner이므로 목적지가 아니다.- UNSUPPORTED_IMPL_DECISION: double-submit vs synchronizer token 선택은 본 Sources 직접 근거 없음 — Spring 기본은 synchronizer. trade-off: double-submit 은 stateless(세션 불요)하나 XSS 에 상대적으로 약함.
- 저장측 요구:
SameSite=Strict+ double-submit CSRF token(요청 header) + Origin/Referer 검증.
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외 실패/엣지/다른 계약 의존.
- 실패·엣지 경로:
- XSS 발생 시:
- localStorage/sessionStorage 토큰: 즉시 전량 탈취(+ localStorage 는 영속) — D2 (
OWASP-HTML5-C2/C3) - 메모리 access_token: 런타임 XSS 가 fetch wrapper 후킹 시 세션 내 탈취 가능, 단 영속 X(reload 소멸)
- D7 variant의 HttpOnly cookie: JS가 raw token을 읽지 못해도 XSS가 활성 session으로 요청을 대행할 수 있고 browser 자동 첨부로 CSRF surface가 생김 → 별도 owner 계약 필요
- PKCE verifier(sessionStorage): 탈취돼도 authorization code 없이는 무가치 → token 탈취보다 심각도 낮음(D6 INFERENCE 근거)
- localStorage/sessionStorage 토큰: 즉시 전량 탈취(+ localStorage 는 영속) — D2 (
- reload: 메모리 access_token 소실 → §구현가이드 2 재취득 필수. 재취득 실패 시 재로그인.
- silent renew 실패: Keycloak cross-site + Safari ITP → hidden iframe 이 SSO cookie 못 읽음 → Keycloak 어댑터가 full redirect 로 fallback("silent" 상실). Chrome normal-mode 는 현재 동작(D3a)하나 정책 변동 리스크.
- refresh_token 탈취: 유효기간 내 victim 데이터 접근(D5,
CURITY-BFF-C6) → rotation 재사용 탐지(sibling 위임). - PKCE verifier 소실: full-page redirect 가 메모리 verifier 파괴 → sessionStorage 필수(D6). 콜백에서 verifier 부재 시 token 교환 실패(
PKCE-RFC7636-C4: "Access is denied if they are not equal"). - 동시성: multi-tab 동시 로그인 → sessionStorage 탭 격리로 verifier 충돌 방지(D6 채택 이유); refresh_token grant rotation 시 동시 refresh race(두 번째 요청이 무효화 토큰 사용) — rotation 구현 detail 은 sibling 위임.
- XSS 발생 시:
- 다른 계약 의존:
- raw/branch-notes/feature-keycloak-refresh-token-rotation
D1(rotation 활성화 =Revoke Refresh Token ON+ reuse detection)·D4(rotation flow 4단계: RT 사용→invalidate→재발급→재사용 시 family invalidate) — 본 branch D4/D5 의 mitigation 을 이 sibling 이 owns. 본 branch 는 "rotation 에 의존"만 결정하고 재사용탐지·TTL 은 consume. 그 계약(rotation 활성/family invalidate 범위)이 바뀌면 D4/D5 위험 평가에 영향. - raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce — 실제 SPA 가 본 §구현가이드 배치 명세를 구현. 본 branch 의 §구현가이드 = 그 branch 의 입력 계약.
- raw/branch-notes/feature-keycloak-bff-vs-spa-direct — BFF 대안. 본 branch 는 SPA Direct 전제. BFF 채택 시 토큰이 브라우저에 없어 D1/D2/D6 대부분 무효화.
- D7 variant의 cookie/CSRF 구현 owner는 아직 없음(
OWNER_REQUIRED). 채택 전 별도 계약을 만들어야 하며 pure SPA baseline의 raw/branch-notes/feature-keycloak-spring-rs-audience-validator에 암묵적으로 부과하지 않는다.
- raw/branch-notes/feature-keycloak-refresh-token-rotation
검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| pure SPA에서 access/refresh token을 memory-only로 두고 reload 시 재인증하는 baseline | 문서 근거는 있으나 실 SPA 구현 없음 | raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce에서 로그인→API→reload→user/token 소실→재인증을 E2E 확인 | planned |
| D7 HttpOnly refresh-cookie variant의 server endpoint·CSRF 계약 | 현재 owner와 구현 artifact가 없음 | variant owner branch와 D-row를 먼저 만든 뒤 /refresh Set-Cookie, CSRF negative test, logout/revoke E2E 확인 |
blocked-on-owner |
| Safari cross-site silent renew 실패와 Chrome 조사시점 정책의 runtime 동작 | D3/D3a의 vendor 근거는 확보됐지만 본 topology E2E 미실행 | same-site/cross-site를 나눠 Safari와 Chrome에서 hidden iframe/full redirect를 관측 | planned (source-resolved, runtime-unverified) |
| D7 variant의 CSRF 방어 조합 | pure SPA 범위 밖이고 owner 미정 | owner 지정 후 위협 모델에 맞는 SameSite/CSRF token/Origin 검증과 negative E2E를 명세 | blocked-on-owner |
PKCE code_verifier sessionStorage 선택 |
RFC·OWASP 근거 사슬은 확보됐지만 직접 BCP 권고가 아닌 inference | full-page redirect 전후 verifier 생존과 callback 직후 제거를 E2E 확인 | planned (inference-grounded, runtime-unverified) |
| 본 branch 4 저장소 비교표의 각 셀이 OWASP 또는 OAuth 2.1 draft 의 정확한 quote 로 직접 뒷받침되는지 | 본문 표는 종합 판단 — 셀별 source mapping 부재 | 각 셀마다 supporting claim 표기 또는 본 branch 본문 통찰임을 명시 | planned |
마주친 문제
- 이슈 1: 메모리 저장은 reload 시 토큰을 잃음 → UX 저하 vs 보안 trade-off.
- 원인: SPA가 매 reload마다 새로 부트스트랩되므로 closure 변수는 사라짐
- 시도: (구현 없음)
- 해결: pure SPA baseline은 reload 시 재인증. 무중단 UX가 필수면 D7 TMB/BFF variant를 별도 채택 —
documented-only
묶음
- raw/official-docs/chrome-third-party-cookie-policy-google-official
- raw/official-docs/oauth-v2-1-draft-ietf
- raw/official-docs/owasp-html5-storage-xss-spa
- raw/official-docs/silent-check-sso-third-party-cookies-keycloak-official
- raw/official-docs/third-party-cookie-blocking-safari-webkit-official
본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
근거 자료
- raw/official-docs/owasp-html5-storage-xss-spa
- raw/official-docs/oauth-v2-1-draft-ietf
- raw/company-tech-blogs/curity-bff-pattern-spa
- raw/official-docs/chrome-third-party-cookie-policy-google-official — D3 UNSUPPORTED_DECISION 정정 근거: Chrome 은 2025-04-22 기준 default third-party-cookie blocking 을 롤아웃하지 않음(일반 모드는 여전히 허용, Incognito 모드만 기본 차단)
오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
관련 일일 노트
완료 후 정리
머지/종료 시점에 채움.
/ingest가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크: (미구현 — 문서까지만)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 없음 (P2A는
documented-only범위) - wiki 추출 대상: 현 단계 없음. 추후
wiki/concepts/spa-token-storage-trade-off.md로 합성 후보. - 추출하지 않을 항목: P2A 구현 없음.
documented-only유지.