28 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-vanilla-js-spa-pkce (vanilla JS SPA — Authorization Code + PKCE) | branch-note | raw | BR-KEYCLOAK-PATTERNS-OVERVIEW-003 | project-work-item | keycloak-patterns-overview | WI-KEYCLOAK-PATTERNS-OVERVIEW-003 |
|
|
1 | feature-keycloak-vanilla-js-spa-pkce |
|
|
2026-05-25 | in-progress | bb1be862636aa363e6d10eb54600075ab82106f6c707a98acbfd84a938adf0f4 |
branch: feature-keycloak-vanilla-js-spa-pkce (vanilla JS SPA — Authorization Code + PKCE)
Layer:
raw/branch-notes/— raw/project-notes/keycloak-patterns-overview의WI-KEYCLOAK-PATTERNS-OVERVIEW-003직접 branch. P3A는 실 구현 대상. 본 sub-sub는 학습 + 작업 plan 기록 — 실 구현은/home/donghyeon/workspace/keycloak-patterns/.
부모 (필수)
raw/project-notes/keycloak-patterns-overview
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: vanilla JS PKCE login·token 수령·protected API 200이 재현된다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1 |
AP1은 public client와 Authorization Code + PKCE를 사용한다 | vanilla JS SPA의 Authorization Code + PKCE flow에 적용한다 | raw/project-notes/keycloak-patterns-overview |
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1 |
done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | login·token 수령·protected API 200을 E2E evidence로 사용한다 | 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 |
|---|
없음.
목표
vanilla JS (no React/Vue/Angular)로 OIDC Authorization Code + PKCE 흐름을 직접 구현한다. oidc-client-ts 우선 채택 후, 별도 학습 단계에서 manual crypto.subtle 기반 PKCE 비교 구현. login button → Keycloak redirect → callback → token storage → /api/me 호출 → silent renew → logout 전체 lifecycle.
면접 질문: "PKCE 흐름을 코드로 설명해 주세요."
→ "SPA가 code_verifier 43–128자 랜덤 생성, code_challenge = BASE64URL(SHA256(code_verifier))로 변환합니다. authorize 요청에 code_challenge와 code_challenge_method=S256을 첨부하고, 콜백에서 받은 code로 token 교환할 때 원본 code_verifier를 함께 보냅니다. authorization code interception attack 방어 — public client는 client_secret이 없으므로 PKCE가 사실상 필수입니다."
- 이슈:
- PR: (별도 keycloak-patterns repo)
범위
포함 범위
index.html(login button, logout button,/api/me호출 결과 표시 영역)app.js(oidc-client-tsUserManager사용)callback.html(redirect callback 처리 페이지) — 또는 main page에서?code=...감지- PKCE S256 (oidc-client-ts 내부 처리)
- token storage: in-memory (학습용,
UserManager.events.addUserLoaded(...)로 closure 보관) - silent renew (
automaticSilentRenew: true) Authorization: Bearer ${user.access_token}헤더로/api/me호출- logout button →
signoutRedirect()(Keycloak/logoutendpoint) - (별도 단계) manual PKCE:
crypto.subtle.digest('SHA-256', ...)+ base64url encoding 직접 구현
제외 범위
- React/Vue/Angular framework 사용 (vanilla 학습 목적)
- iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)
- 자체 token storage 암호화
- mobile / native client (PKCE 자체는 동일, 본 sub는 SPA)
근거 (필수, 최소 1개+)
- raw/official-docs/oidc-client-ts-library — oidc-client-ts 공식 (D1·D3 근거: PKCE·refresh·silent iframe 지원)
- raw/official-docs/oauth2-pkce-rfc-7636 — RFC 7636 PKCE (D4·§구현가이드 5 근거: verifier/challenge·S256 공식)
- raw/official-docs/oauth-v2-1-draft-ietf — OAuth 2.1 draft (2026-07-18
/branch-spec자동조사로 추가: D4 implicit 제거OA21-C2, D5 redirect_uri exact-matchOA21-C5근거) - raw/official-docs/keycloak-client-pkce-method-enforcement-official — Keycloak client 등록 시 "PKCE method" 옵션 확인 근거 (server-side 강제는 owner sibling raw/branch-notes/feature-keycloak-realm-client-export 소유;
KC-PKCE-C1/C3)
TODO
npm init+oidc-client-ts설치 — 등급:plannedindex.html: login button (id=login), logout button (id=logout), result 영역 (id=result) — 등급:plannedapp.js:UserManager인스턴스 — 등급:plannedauthority: 'http://localhost:8080/realms/keycloak-patterns'client_id: 'spa-client'redirect_uri: 'http://localhost/callback.html'post_logout_redirect_uri: 'http://localhost/'response_type: 'code'scope: 'openid profile'automaticSilentRenew: true
- login button click →
userManager.signinRedirect()— 등급:planned callback.html:<script>→new UserManager(config).signinRedirectCallback().then(user => location.href='/')— 등급:planned- main page load 시
userManager.getUser()→ memory user가 있으면 runtime backend URL로 API 호출, reload로 없으면 재인증 — 등급:planned fetch('http://localhost:8081/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })또는 동일 값을 주입한runtimeConfig.backendBaseUrl사용 → JSON render — 등급:planned- logout button click →
userManager.signoutRedirect()— 등급:planned - silent renew 검증: access token 만료 (5분) 직전 자동 갱신 발생 → DevTools Network 탭에서
/token(grant_type=refresh_token) 호출 확인 — 등급:planned - CORS 검증: nginx 80 → backend 8081 호출 시 preflight 통과 — 등급:
planned - (별도 단계) manual PKCE 구현: — 등급:
plannedcrypto.getRandomValues(new Uint8Array(32))→ base64url →code_verifiercrypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))→ base64url →code_challengesessionStorage.setItem('pkce_verifier', verifier)- manual token exchange는 OIDC discovery의
token_endpointabsolute URL 사용 (grant_type=authorization_code+code_verifier)
- (별도 단계) oidc-client-ts vs manual 동작 비교 — 등급:
planned
진행 중 메모
oidc-client-ts우선 채택 이유: production-grade silent renew / state / nonce / token validation을 한 줄로 처리. 학습 후 manual로 내부 동작 검증.- token storage: pinned oidc-client-ts version의
stateStore/userStoredefault는 아직 미검증이다. default에 의존하지 않고 token/user store는 explicit in-memory로 선택한다. redirect transaction state만 full-page callback 생존을 위해 explicit sessionStorage에 두고 callback 직후 정리한다. - redirect_uri:
http://localhost/callback.html. Keycloak client Valid Redirect URIs에 정확히 등록되어야 함 (sub-5-2 참조). - silent renew: refresh token rotation ON이면 매 갱신마다 새 refresh token. rotation 동작 검증은 sub-5-6에서.
scope=openid profile:openid는 OIDC 식별,profile은preferred_username등 user claim 포함.- manual PKCE 학습 가치:
code_challenge계산, state/nonce 관리, callback URL parsing을 직접 다뤄야 OIDC 흐름이 머리에 그려짐.
결정 사항 (decisions)
- 2026-05-25:
oidc-client-ts우선, manual은 별도 단계. 이유: 작동하는 환경을 먼저 만들고 내부 동작은 비교 학습. - 2026-05-25: token storage in-memory (학습용). 이유: localStorage XSS 우려 — prod에서는 BFF 패턴이 더 안전. 학습 단계에서 token 흐름이 명확히 보이도록 in-memory 채택.
- 2026-05-25:
automaticSilentRenew: true. 이유: refresh token rotation 동작 시연 (sub-5-6) 자동화. - 2026-05-25:
response_type=code고정 (legacyimplicitflow 미사용). 이유: RFC 8252 / OAuth 2.1 권장 — implicit flow는 deprecated. - 2026-05-25: redirect_uri는
http://localhost/callback.html단일. 이유: callback page 분리 → main page 로딩 흐름과 분리해 디버깅 쉬움. - 2026-07-18 (
/branch-spec자동조사 보강): D4 를UNSUPPORTED에서 해소 — implicit deprecation 의 공식 근거를 raw/official-docs/oauth-v2-1-draft-ietfOA21-C2(Implicit + ROPC grant 제거) +OA21-C1(PKCE MUST all clients)로 확정. 기존 RFC 8252 추정 대신 OAuth 2.1 표준 직접 인용. - 2026-07-18 (
/branch-spec자동조사 보강): D5 를UNSUPPORTED에서 해소(부분) — redirect_uri exact-match 요구는OA21-C5(registered redirect URI 와 exact match 안 하면 MUST 거부)로 확정. 단 단일 callback page 분리 vs main-page?code=감지 는 표준 요구가 아닌 디버깅 편의 판단이므로UNSUPPORTED_IMPL_DECISION(§구현가이드 2)으로 강등. - 2026-07-18 (
/branch-spec자동조사 보강): D2 를UNSUPPORTED에서 해소(위임) — token 저장 위치 trade-off 는 owner sibling raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff (D1/D2/D6, OWASP·Curity·OAuth 2.1 근거)가 소유. 본 branch 는 그 분석을 재진술하지 않고 학습 단계용 in-memory 지점을 선택(선택 조건 = 학습 vs prod). Reference-Only(rules/consistency-contract).
결정-근거 매핑
각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
Decision ID는 이 branch-note 안에서 안정적으로 유지한다.Supporting Claims는raw/<category>/<slug>.md#<CLAIM-ID>형식.선택 조건열(R2): 이 조건일 때 이 결정 / 다른 조건이면 어떤 대안. 분기 없으면 N/A.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | oidc-client-ts 우선 채택 (manual PKCE 는 비교 학습용 별도 단계) |
작동하는 baseline 을 먼저 확보하고 내부 동작을 비교 학습 → library 우선. 브라우저 내부 crypto/state/nonce 를 직접 다뤄 학습 → manual crypto.subtle 구현(§구현가이드 5) |
raw/official-docs/oidc-client-ts-library.md#OIDCTS-C3 (PKCE 지원 명시), raw/official-docs/oidc-client-ts-library.md#OIDCTS-C2 (OAuth 2.1 지속 지원 protocol 만) |
official-vendor-doc |
OIDCTS-C1 이 origin project 2021-06 개발 중단을 명시 — fork 의 active maintenance / 보안 패치 상태는 별도 확인 |
| D2 | pure SPA token storage = explicit in-memory (access/refresh 모두), reload 시 재인증 | AP1 pure SPA baseline이면 default store에 의존하지 않고 memory-only. HttpOnly refresh cookie는 raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D7의 TMB/BFF variant | 위임: raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D1/D2/D6 | delegated (official-reference via owner) |
실행 중 XSS 노출은 남는다. redirect transaction state와 token user store를 구분해야 함 |
| D3 | automaticSilentRenew: true (refresh token rotation 자동화) |
refresh token rotation 동작을 자동 시연하려는 학습 목표 → 활성. Keycloak cross-site + Safari 배포로 iframe silent renew 가 구조적으로 실패하는 환경 → refresh_token grant 직접 사용 우선(owner token-storage D3) | raw/official-docs/oidc-client-ts-library.md#OIDCTS-C4 (Refresh Token Grant 지원), raw/official-docs/oidc-client-ts-library.md#OIDCTS-C5 (Silent Refresh Token in iframe Flow 지원). 실패 조건 위임: raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D3(Safari)·D3a(Chrome) |
official-vendor-doc |
OIDCTS-C5 의 "Does not prove": 3rd-party cookie 차단 환경(Safari ITP / Chrome Incognito)에서 iframe flow 보장 안 함. automaticSilentRenew 가 iframe vs refresh_token grant 중 무엇을 default 로 쓰는지 미확정(Claims To Verify). rotation default 활성은 Keycloak server-side 설정 의존 |
| D4 | response_type=code 고정 (implicit flow 미사용) |
public client(SPA)의 표준 flow → 항상 code + PKCE. implicit 은 OAuth 2.1 에서 제거되어 대안이 아님 | raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C2 (Implicit + ROPC grant 제거), raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1 (PKCE MUST all clients), raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C1 (public client + code grant → interception → PKCE 전제) |
official-standard |
(이전 UNSUPPORTED 해소 — OA21-C2 가 implicit 제거를 직접 증명) code_verifier 길이/문자셋(RFC 7636 §4.1)은 본 인용 범위 밖 |
| D5 | redirect_uri = http://localhost/callback.html 단일 (exact-match) |
authorization server 는 registered redirect URI 와 exact match 안 하면 MUST 거부 → 정확한 단일 URI 등록. callback 전용 page 분리 vs main page ?code= 감지 는 디버깅 편의 판단(임의) |
raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C5 (redirect URI exact-match MUST) |
official-standard (exact-match 요구); callback page 분리는 UNSUPPORTED_IMPL_DECISION(§구현가이드 2) |
exact-match 자체는 OA21-C5 로 증명. 단일 callback page 분리는 표준 요구 아님 — main-page handling 도 유효. 등록된 redirect URI 실체는 owner sibling raw/branch-notes/feature-keycloak-realm-client-export 소유 → 그 등록값 변경 시 D5 영향 |
구현 가이드
본 branch 는
documented-only(실 구현 repokeycloak-patterns/아직 부재 —NO_GROUND_TRUTH). 아래는 다음 구현자가 되묻지 않고 코드를 작성할 수준의 사전 명세. 각 sub-section 은 Decision ID + Supporting Claim ID 를 Trace 하거나UNSUPPORTED_IMPL_DECISION/OUT_OF_BRANCH_SCOPE라벨을 단다(CLAUDE.md §15.5 3-rule).
1. UserManager 설정 명세 (config object)
Trace: D1 (
OIDCTS-C3PKCE 자동), D3 (OIDCTS-C4/C5refresh·silent), D4 (OA21-C1PKCE MUST,OA21-C2implicit 제거,PKCE-RFC7636-C1), D5 (OA21-C5exact-match)
- OUT_OF_BRANCH_SCOPE:
client_id(spa-client) 값·Valid Redirect URIs 등록은 owner sibling raw/branch-notes/feature-keycloak-realm-client-export#D2(현재 학습용http://localhost/*·http://127.0.0.1/*wildcard 등록), server-side PKCE method=S256 강제는 그#D1(KC-PKCE-C1/C3) 소유. 본 branch 는 그 등록값을 consume 만 한다.
| config key | 값 | Trace | 라벨 |
|---|---|---|---|
authority |
http://localhost:8080/realms/keycloak-patterns |
realm URL → .well-known/openid-configuration 자동 조회. iss 검증 위해 hostname 이 KC_HOSTNAME 과 일치 필수(OIDCTS docs 경고: authority↔KC_HOSTNAME) → iss-claim-hostname-mismatch #D1 의존 |
consume (iss-claim #D1) |
client_id |
spa-client |
client 등록 owner | OUT_OF_BRANCH_SCOPE (realm-client-export #D1/#D2) |
redirect_uri |
http://localhost/callback.html |
D5 / OA21-C5 (exact-match); 등록은 realm-client-export #D2 |
— |
post_logout_redirect_uri |
http://localhost/ |
logout redirect | UNSUPPORTED_IMPL_DECISION: 루트 / 로 복귀는 임의 — trade-off: 전용 logged-out page 분리하면 UX 명확하나 파일 1개 추가 |
response_type |
code |
D4 / OA21-C2(implicit 제거)·PKCE-RFC7636-C1 |
— |
scope |
openid profile |
openid=OIDC 식별, profile=preferred_username claim |
UNSUPPORTED_IMPL_DECISION: profile 외 scope(email/roles 등)는 /api/me 요구에 따라 — trade-off: 최소 scope 원칙 vs claim 부족 시 재요청 |
automaticSilentRenew |
true |
D3 / OIDCTS-C4/C5 |
— |
2. 페이지·이벤트 wiring 명세 (index.html
Trace: D1 (library
signinRedirect/signinRedirectCallback/signoutRedirect), D5 (callback URI)
- UNSUPPORTED_IMPL_DECISION: (a) DOM element id 명명(
login/logout/result)은 임의 — trade-off: 짧은 고정 id 는 단순하나 다중 위젯 시 충돌 위험. (b) callback 전용callback.html분리 vs main page 에서?code=감지는 D5 Open Risk 의 디버깅 편의 판단 — trade-off: 분리는 main 로딩 흐름과 격리돼 디버깅 쉽지만 redirect_uri·정적 파일 1개 추가; main-page handling 은 파일 최소이나 초기 로드 로직에 code 교환이 섞임.
| 대상 | 명세 |
|---|---|
index.html |
<button id="login">, <button id="logout">, <pre id="result"> |
app.js (main load) |
userManager.getUser() → user 있으면 §4 /api/me 호출; #login.onclick → userManager.signinRedirect(); #logout.onclick → userManager.signoutRedirect() |
callback.html |
<script> → new UserManager(config).signinRedirectCallback().then(() => location.href = '/') (code→token 교환 후 main 복귀) |
3. explicit in-memory token store 명세
Trace: D2 (위임 raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D1/D2 — localStorage 회피, access=memory)
pinned version default는 미검증이므로 active baseline은 default와 무관하게 explicit store를 지정한다.
userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() })— access/refresh/id token을 memory-only로 유지.stateStore: new WebStorageStateStore({ store: window.sessionStorage })— full-page redirect의state/transaction만 생존시키며 callback 성공 뒤 정리. token persistence 용도가 아니다.- reload 뒤
getUser()가 비면 silent 복구를 기본 가정하지 않고 재인증한다.
4. /api/me 호출 + silent renew 검증 명세
Trace: D3 (silent renew), D1 (
user.access_token)
- OUT_OF_BRANCH_SCOPE: nginx 80 → backend 8081 의 CORS preflight 정책(Authorization 헤더 허용·credentials)은 backend Spring Security 결정 → RS 계열 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 로 위임. 본 branch 는 "Bearer 헤더로 호출한다"는 client 측 요구만 남긴다.
- static-only nginx/3-port topology에서는
runtimeConfig.backendBaseUrl기본값http://localhost:8081을 주입하고fetch(runtimeConfig.backendBaseUrl + '/api/me', ...)로 호출한다. relative/api/me는 nginx:80로 가므로 사용하지 않는다. - silent renew 검증(§Claims To Verify): access token 만료 직전 DevTools Network 에서
/token(grant_type=refresh_token) 호출 vs hidden iframe 로드 관찰 →automaticSilentRenew의 실제 메커니즘 확정.
5. (별도 학습 단계) manual PKCE 구현 명세
Trace:
PKCE-RFC7636-C2(verifier 생성·기록 + challenge 도출),PKCE-RFC7636-C3(code_challenge = BASE64URL-ENCODE(SHA256(ASCII(verifier)))),PKCE-RFC7636-C4(불일치 시 access 거부), verifier 저장 위치는 위임 raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D6(full-page redirect 전제 → sessionStorage)
- UNSUPPORTED_IMPL_DECISION: verifier sessionStorage 키명(
pkce_verifier)은 임의 — trade-off: 고정키는 단순하나 multi-tab 동시 로그인 시 충돌(owner D6 는state포함 키를 대안 제시).
| 단계 | 명세 | Trace |
|---|---|---|
| verifier 생성 | crypto.getRandomValues(new Uint8Array(32)) → base64url → code_verifier (43–128 char) |
PKCE-RFC7636-C2 |
| challenge 도출 | crypto.subtle.digest('SHA-256', TextEncoder().encode(verifier)) → base64url → code_challenge, code_challenge_method=S256 |
PKCE-RFC7636-C3 |
| verifier 보관 | sessionStorage.setItem('pkce_verifier', verifier) — 토큰 교환 성공 즉시 removeItem |
owner token-storage D6 |
| token 교환 | discovery metadata의 absolute token_endpoint로 POST. relative /token 금지 |
PKCE-RFC7636-C4 + static-only topology |
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외 실패/엣지/다른 계약 의존.
- 실패·엣지 경로:
- redirect_uri mismatch (
localhostvs127.0.0.1, 또는/callback.html오타): authorization server 가 exact-match 실패로 요청 거부(D5 /OA21-C5) → authorize 단계에서 에러. 등록값은 owner realm-client-export 소유. - CORS preflight 실패: nginx 80 → backend 8081 의
/api/me호출 시 backend CORS 미설정이면 preflight(OPTIONS) 차단 → §구현가이드 4 OUT_OF_BRANCH_SCOPE(RS branch). - silent renew 실패: Keycloak cross-site + Safari ITP → hidden iframe 이 SSO cookie 못 읽음 → 어댑터가 full redirect fallback("silent" 상실). Chrome 일반 모드는 현재 동작(owner D3a)하나 정책 변동 리스크. → refresh_token grant 직접 사용으로 우회(owner token-storage D3).
- in-memory 토큰 reload 소실: 페이지 새로고침 시 access/refresh token이 함께 소멸 → baseline은 재인증. silent SSO는 별도 조건부 비교다.
- manual PKCE verifier 소실: full-page redirect 가 메모리 verifier 파괴 → sessionStorage 필수(owner D6). 콜백에서 verifier 부재 시 token 교환 실패(
PKCE-RFC7636-C4: "Access is denied if they are not equal"). - access token 만료 vs API 호출 race:
/api/me호출 순간 토큰 만료면 401 → silent renew 후 재시도 필요(구현 시 retry wrapper 고려,needs-confirmation).
- redirect_uri mismatch (
- 다른 계약 의존:
- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff
D1(access=memory)·D2(localStorage 금지)·D6(verifier=sessionStorage) — 본 branch 의 token/verifier 저장 배치는 이 owner 의 trade-off 분석을 consume. 그 결정이 바뀌면(예: prod 에서 httpOnly cookie 필수화) 본 branch 저장 명세 재검토. - raw/branch-notes/feature-keycloak-realm-client-export
#D1(server-side PKCE method=S256 강제,KC-PKCE-C1/C3)·#D2(Valid Redirect URIs 등록 — 현재 학습용http://localhost/*·127.0.0.1/*wildcard) 를 owns. 등록된 redirect URI 가 바뀌면 D5·§구현가이드 1 영향. - raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch
#D1(KC_HOSTNAME=localhost로 issuer URL 고정) — 본 branchauthorityhostname(localhost)이 이 값과 일치해야 발급 token 의iss가 backend RS 검증을 통과(OIDCTSdocs:authority↔KC_HOSTNAME일치 경고). 불일치 시/api/me가 401 → 원인이 CORS(§구현가이드 4)가 아니라issmismatch 임을 구분해 진단. - raw/branch-notes/feature-keycloak-refresh-rotation-and-logout (및 raw/branch-notes/feature-keycloak-refresh-token-rotation) —
automaticSilentRenew가 refresh_token grant 로 동작 시 rotation 계약(재사용 탐지·TTL)에 의존. rotation 활성/family invalidate 범위가 바뀌면 D3 갱신 동작 영향. - raw/branch-notes/feature-keycloak-spring-rs-audience-validator —
/api/me의 Bearer 검증·CORS·audience 정책을 owns(§구현가이드 4 OUT_OF_BRANCH_SCOPE). - raw/branch-notes/feature-keycloak-pkce-flow-stages — PKCE (A)~(E) 단계 분해를 owns. 본 branch §구현가이드 5 는 그 단계 설계의 vanilla-JS 구현.
- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff
검증해야 할 주장
공식 문서 근거가 있어도 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
oidc-client-ts 의 automaticSilentRenew: true 가 iframe 기반이 아닌 refresh token grant 로 동작한다 |
OIDCTS-C4 와 C5 가 둘 다 지원 protocol 로 나열됨 — 어느 메커니즘이 default 인지 본 인용 범위 밖 | dev 환경에서 token 만료 직전 DevTools Network 탭 캡처 → /token (grant_type=refresh_token) 호출 확인 vs iframe 로드 확인 |
planned |
| Keycloak SPA client 의 access token 만료가 5분이며 그 직전에 silent renew 트리거 | 본 branch 의 Sources 는 Keycloak 의 token lifetime default 를 다루지 않음 | dev Keycloak realm settings > Tokens > Access Token Lifespan 확인 | planned |
crypto.subtle.digest('SHA-256', ...) + base64url 로 manual PKCE 구현이 oidc-client-ts 와 동일한 challenge 값 생성 |
RFC 7636 PKCE-RFC7636-C3 가 BASE64URL-ENCODE(SHA256(ASCII(verifier))) 공식 정의. 두 구현의 byte-level 일치는 실측 필요 |
동일 verifier 입력으로 manual 함수와 oidc-client-ts 내부 함수 결과 비교 | planned |
| nginx 80 → backend 8081 CORS preflight 통과 (Authorization 헤더 허용 + credentials 정책) | 본 branch 의 Sources 는 CORS 정책을 다루지 않음 (RS branch 소유) | backend Spring Security CORS 설정 + DevTools Network preflight 응답 확인 | planned |
| explicit in-memory userStore가 access/refresh token을 persistent storage에 남기지 않고 reload 뒤 재인증을 요구 | active baseline은 정했지만 pinned version runtime 미검증 | 로그인 후 local/sessionStorage token 검색 → reload 뒤 getUser() null → 재인증 E2E |
planned |
Keycloak client "PKCE method"=S256 토글이 code_challenge_method=plain 요청을 실제로 거부한다 |
KC-PKCE-C3 의 "applies... S256" 은 강제를 암시할 뿐 reject/error 를 명시 안 함(그 raw 의 Usage Boundaries) — server-side 강제는 realm-client-export owns |
dev Keycloak 에서 PKCE method=S256 설정 후 plain 요청 → redirect 에러 파라미터/HTTP status 확인 | needs-confirmation |
pinned oidc-client-ts의 default stateStore/userStore 종류와 차이 |
active baseline은 explicit store라 default에 의존하지 않지만 비교 설명의 사실 정확성은 미확인 | pinned version docs와 runtime storage key를 각각 확인 | needs-confirmation |
마주친 문제
- (구현 시작 후 추가)
localhostvs127.0.0.1redirect_uri mismatch 예상. - (구현 시작 후 추가) CORS preflight 실패 예상 (backend CORS 설정 누락 시).
- (구현 시작 후 추가) silent renew가 iframe 기반이면 third-party cookie 차단 이슈 — refresh token 기반인지 확인.
묶음
- raw/official-docs/keycloak-client-pkce-method-enforcement-official
- raw/official-docs/oauth-v2-1-draft-ietf
- raw/official-docs/oauth2-pkce-rfc-7636
- raw/official-docs/oidc-client-ts-library
- raw/official-docs/owasp-html5-storage-xss-spa
본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
관련 일일 노트
완료 후 정리
로컬 검증(login → /api/me → silent renew → logout 전체 흐름) 통과 시
planned→actually-implemented/locally-verified승급.
- PR 링크: (별도 keycloak-patterns repo)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 로컬 docker-compose / 단일 EC2 시뮬레이션
- wiki 추출 대상:
actually-implemented항목: (구현 후 채움)locally-verified항목: (구현 후 채움)prod-verified항목: (없음)
- 추출하지 않을 항목: 현재 전부
planned.