--- title: branch / feature-keycloak-vanilla-js-spa-pkce (vanilla JS SPA — Authorization Code + PKCE) source_type: branch-note status: raw id: BR-KEYCLOAK-PATTERNS-OVERVIEW-003 kind: project-work-item project: keycloak-patterns-overview work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-003 inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1, DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1] refines: [] overrides: [] depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-002] contract_packet: 1 branch: feature-keycloak-vanilla-js-spa-pkce parent_branch: related_projects: [keycloak-patterns] tags: [branch, keycloak-patterns, p3a, implementation, vanilla-js, spa, pkce, oidc-client-ts] created: 2026-05-25 target_merge: status_label: in-progress contract_packet_sha256: 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-ts` `UserManager` 사용) - `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 `/logout` endpoint) - (별도 단계) 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-match `OA21-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` 설치 — 등급: `planned` - [ ] `index.html`: login button (id=`login`), logout button (id=`logout`), result 영역 (id=`result`) — 등급: `planned` - [ ] `app.js`: `UserManager` 인스턴스 — 등급: `planned` - `authority: '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`: `