--- title: branch / feature-keycloak-pkce-flow-stages (PKCE 4단계 — verifier/challenge/auth/exchange) source_type: branch-note status: raw id: BR-KEYCLOAK-CHILD-B7701136 kind: branch-child project: keycloak-patterns-overview work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-020 inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1] refines: [] overrides: [] depends_on: [] contract_packet: 1 branch: feature-keycloak-pkce-flow-stages parent_branch: feature-keycloak-patterns related_projects: [keycloak-patterns] tags: [branch, keycloak-patterns, p2a, pkce, oauth2, rfc-7636, spa] created: 2026-05-25 target_merge: status_label: in-progress contract_packet_sha256: 018199eabc07fd85c9fcf91fdfe596cec5c1264b6797f184b65568c51f8896bc --- # branch: feature-keycloak-pkce-flow-stages — PKCE 단계별 (code_verifier > Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-patterns]] governance Work Item의 child. P2A 의미 계약은 [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]]를 참조한다. > **목적**: RFC 7636 PKCE의 4단계를 입력/출력/보안 의미까지 단계별로 정확히 설명할 수 있게 한다. > `status_label`: `in-progress` | `review` | `merged` | `abandoned` ## 부모 (필수) [[raw/branch-notes/feature-keycloak-patterns]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: project governance hub가 AP1~AP4 taxonomy와 child progress index를 유지한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1` | canonical 분류축은 AP1~AP4 인증 통합 아키텍처와 cross-cutting 변형이다 | AP1 SPA-direct 변형의 PKCE 단계별 계약에 적용한다 | [[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 | |---|---|---|---|---| 없음. ## 목표 P2A는 SPA(public client)가 client secret을 보관할 수 없으므로 authorization code 탈취 시 누구나 토큰을 받아낼 수 있다. PKCE는 code-to-token 단계에서 **이 코드를 발급받은 동일 클라이언트만 토큰을 받을 수 있도록** `code_verifier`/`code_challenge` 바인딩을 추가하는 메커니즘이다. 핵심 질문: - `code_verifier` 형식은 왜 43~128자 unreserved character로 제한되는가? (entropy 보장 + URL-safe) - `S256`과 `plain`의 차이는? 왜 OAuth 2.1은 `S256`을 강제하는가? - `state` / `nonce`는 PKCE와 어떻게 다른 역할인가? - `code_verifier`가 localStorage에 노출되면 PKCE는 어떤 의미인가? (= 거의 무의미) 본 sub-sub-branch는 **각 단계의 입력/출력/공격 모델/방어 효과**를 한 줄씩 정리한다. - 이슈: (학습 노트, 이슈 없음) - PR: (구현 없음) ## 범위 ### 포함 범위 - (본문 해당 섹션에서 다룬 항목 참조) ### 제외 범위 - (명시 필요) ## 근거 (필수, 최소 1개+) - [[raw/official-docs/oauth2-pkce-rfc-7636]] — RFC 7636 PKCE 본문 (verifier/challenge 정의, S256 / plain) - [[raw/official-docs/oauth-v2-1-draft-ietf]] — OAuth 2.1 draft (PKCE mandatory, S256 강제) - [[raw/official-docs/keycloak-securing-apps-overview-official]] — Keycloak SPA client 설정 - [[raw/official-docs/keycloak-client-pkce-method-enforcement-official]] — Keycloak "PKCE method" Admin UI 옵션 (Capability Config) — D1/D5 관련 UI 라벨 정정 근거 ## TODO 각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation` - [ ] **Step 1: code_verifier 생성** — 등급: `documented-only` - 형식: 43~128 chars, unreserved = `[A-Z] [a-z] [0-9] - . _ ~` (RFC 7636 §4.1) - entropy: 최소 256 bits 권장 (`crypto.getRandomValues(32 bytes)` → base64url) - 저장 위치: **메모리 또는 sessionStorage**. localStorage 절대 금지 (XSS 노출 시 PKCE 무력화) - [ ] **Step 2: code_challenge 계산** — 등급: `documented-only` - `code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` (S256) - `S256` vs `plain`: `plain`은 challenge = verifier (해시 안 함). MITM이 challenge만 보고 verifier 추론 가능 → **OAuth 2.1은 S256 강제** - Keycloak client 설정: Capability config의 `PKCE method = S256` 지정 - [ ] **Step 3: Authorization Request (`/auth`)** — 등급: `documented-only` - 추가 파라미터: `code_challenge`, `code_challenge_method=S256`, `state`, `nonce` - `state`: CSRF 방지 (redirect 응답이 본인이 시작한 것인지 확인) - `nonce`: ID token replay 방지 (OIDC 한정, OAuth2만이면 불필요) - 입력: client_id / redirect_uri / scope / state / code_challenge / method - 출력: redirect with `?code=&state=` - [ ] **Step 4: Token Request (`/token` exchange)** — 등급: `documented-only` - 입력: `grant_type=authorization_code` + `code` + `redirect_uri` + `client_id` + `code_verifier` - Keycloak 측 검증: `SHA256(verifier) == 저장된 challenge` 비교 - 출력: `access_token` / `id_token` / `refresh_token` / `expires_in` - 실패 시: `invalid_grant` 응답 - [ ] **만료 / 재시도 시나리오** — 등급: `documented-only` - `code` TTL: Keycloak 기본 60s. 만료 시 `/auth`부터 재요청 (verifier도 새로 생성) - 재사용: authorization code는 **1회용**. 같은 code로 두 번 `/token` 호출 시 두 번째는 거부 + 발급된 토큰 invalidate (RFC 6749 §4.1.2) - [ ] **함정 정리표** — 등급: `documented-only` - verifier를 localStorage에 → XSS로 탈취 → PKCE 무의미 - challenge_method 누락 → Keycloak이 `plain`으로 fallback → S256 강제 설정 필요 - state 검증 누락 → CSRF로 공격자 코드 주입 가능 - redirect_uri exact match 누락 → open redirect 공격 ## 진행 중 메모 작업하며 떠오른 메모. 자유 형식. - `code_verifier` length 43은 base64url(32 bytes) 결과 길이와 일치. 보통 32 random bytes로 생성하면 OK. - 대상 Keycloak UI에서는 Capability config의 `PKCE method`를 `S256`으로 지정한다. 버전별 UI 차이는 생성된 realm export의 client 설정과 함께 대조하며, 미설정 시 실제 허용 동작은 실측 전까지 단정하지 않는다. - `state` random 값은 PKCE와 독립. PKCE = code↔token 바인딩, state = response↔request 바인딩. ## 결정 사항 (decisions) > 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. - 2026-05-25: PKCE method는 **`S256`만** 정리 대상. `plain`은 OAuth 2.1에서 사실상 deprecated이므로 비교용 1줄 언급만. - 2026-05-25: `code_verifier` 저장 위치는 **메모리 또는 sessionStorage** 권장으로 기록. localStorage는 위험성 명시. - 2026-05-25: 본 sub-sub-branch는 PKCE 4단계 자체에 집중. token 저장은 [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]]에서. ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. > `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다. > `Supporting Claims` 는 `raw//.md#` 형식. | Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---| | D1 | PKCE method = `S256` 만 정리 대상 (`plain` 은 비교용 1줄). Keycloak client 설정에서 PKCE method 옵션(정식 UI 라벨 "PKCE method")을 S256 으로 지정 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식: `BASE64URL-ENCODE(SHA256(ASCII(verifier)))`), `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1` (OAuth 2.1: "Clients MUST use code_challenge and code_verifier ..."), `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1` (Admin UI 옵션 정식 명칭·위치), `#KC-PKCE-C3` (S256 선택 시 서술) | `official-standard + official-standard + official-vendor-doc` | OA21-C1 은 PKCE 사용 자체를 MUST 로 강제하지만 "S256 강제 / plain 금지" 라는 정확한 문장은 OA21-C1 인용에 포함 안 됨 — §7.5.1 예외 조건 확인 필요. 단, RFC 7636 + OAuth 2.1 종합 권고로 보면 정당. **2026-07-17 업데이트**: `KC-PKCE-C1` 이 UI 라벨 오류를 정정("Proof Key for Code Exchange Code Challenge Method" 가 아니라 "PKCE method", Capability Config 섹션)했으나, `KC-PKCE-C3` 은 "Keycloak applies... S256" 이라고만 서술 — **S256 설정 시 `code_challenge_method=plain` 요청을 실제로 거부(reject)한다는 명시적 문장은 여전히 없음**. 아래 Claims To Verify 의 "plain 메서드 요청을 거부" 항목은 `needs-confirmation` 유지 | | D2 | `code_verifier` 저장 위치 = 메모리 또는 sessionStorage 권장 (localStorage 금지) | UNSUPPORTED_DECISION | — | 본 branch 의 Sources (RFC 7636, OAuth 2.1 draft, Keycloak securing-apps) 어느 곳도 localStorage vs sessionStorage 의 XSS 노출 차이를 직접 다루지 않음. OWASP XSS 가이드 / RFC 9700 (OAuth 2.0 Security BCP) 추가 필요 | | D3 | 본 sub-sub-branch 는 PKCE 4단계 자체에 집중 (token 저장은 sibling branch 분리) | (스코프 결정 — 단일 source claim 으로 정당화 불필요) | N/A (scope decision) | scope 분리 자체는 evidence-based 가 아닌 작업 구조 결정 | | D4 (TODO 표 step 1) | code_verifier 형식 43~128 chars unreserved, entropy 256 bits 권장 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C2` (verifier/challenge 생성 정의) | `official-standard` (간접 — RFC §4.1 구체 spec 은 본 branch Sources 의 verbatim 인용 표에 미포함, raw 의 "Usage Boundaries" 가 §4.1 추가 발췌 필요로 명시) | RFC 7636 §4.1 의 정확한 character set/length 는 PKCE-RFC7636-C1~C5 verbatim 인용에 직접 포함 안 됨 — raw 의 "Usage Boundaries" 와 "메모" 가 이 한계를 명시. 별도 §4.1 발췌 추가 권장 | | D5 (TODO 표 step 2) | S256 공식 `code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` + 대상 Keycloak Capability config의 `PKCE method = S256` 지정 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식), `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1`, `#KC-PKCE-C3` (대상 UI 라벨·S256 설정) | `official-standard + official-vendor-doc` | Keycloak 버전별 UI 라벨·내부 JSON key 차이는 생성된 realm export와 대조 필요. S256 설정 시 `plain` 또는 PKCE 없는 요청의 실제 거부 응답도 `needs-confirmation` | | D6 (TODO 표 step 4) | Token exchange 시 Keycloak 이 `SHA256(verifier) == 저장된 challenge` 비교 후 실패 시 `invalid_grant` | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C4` (server 가 verifier 변환 후 challenge 와 비교, 불일치 시 access 거부) | `official-standard` | PKCE-RFC7636-C4 의 "Does not prove" 가 명시: 거부 응답의 정확한 error code / HTTP status 는 본 인용 범위 밖. `invalid_grant` 매핑은 RFC 6749 영역 (별도 raw 필요) | ## 구현 가이드 ### 1. PKCE transaction stage 계약 > **Trace**: D4 + `PKCE-RFC7636-C2`, D5 + `PKCE-RFC7636-C3` / `KC-PKCE-C1` / `KC-PKCE-C3`, D6 + `PKCE-RFC7636-C4`만 구현 근거로 사용한다. > > - **UNSUPPORTED_IMPL_DECISION**: D2의 verifier 저장 위치 선택은 현재 외부 claim이 없다. 이 branch에서 새 storage policy를 구현 명세로 고정하지 않고, [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — token custody owner를 따른다. | Stage | 입력 → 출력 | 구현·검증 경계 | Trace | |---|---|---|---| | verifier 생성 | CSPRNG 입력 → transaction별 `code_verifier` | 43~128자·character set은 D4의 간접 근거 한계를 유지하고, RFC §4.1 직접 claim 보강 전에는 `documented-only`다. | D4 / `PKCE-RFC7636-C2` | | challenge + authorization | verifier → S256 challenge와 authorization request | D5의 공식으로 challenge를 계산하고 대상 Keycloak UI의 `PKCE method`를 S256으로 설정한다. `plain`/무-PKCE 요청의 실제 거부는 실측 전 단정하지 않는다. | D5 / `PKCE-RFC7636-C3`, `KC-PKCE-C1`, `KC-PKCE-C3` | | token exchange | authorization code + 동일 verifier → token 또는 access 거부 | server-side 변환값 불일치를 거부하는 것까지만 단언한다. 정확한 Keycloak error code·HTTP status는 별도 검증 결과로 채운다. | D6 / `PKCE-RFC7636-C4` | ## 엣지·실패·의존 - **실패·엣지 경로**: verifier 불일치 시 access를 거부해야 한다(D6). `invalid_grant`와 HTTP status는 현재 source 범위 밖이므로 test expected value를 고정하기 전에 dev Keycloak 응답을 캡처한다. - **실패·엣지 경로**: Keycloak의 `PKCE method = S256` 설정이 `plain` 또는 PKCE 없는 요청을 실제로 거부하는지는 `needs-confirmation`이다. 설정 전후 realm export와 token exchange 응답을 함께 대조한다. - **실패·엣지 경로**: authorization code TTL 60초·재사용 시 기존 token 무효화는 현재 근거가 부족하다. 아래 Claims To Verify가 닫힐 때까지 구현 상수나 확정 동작으로 승격하지 않는다. - **다른 계약 의존**: [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] D1 — SPA-direct 배치 선택 owner. [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — token custody owner. 본 branch는 두 foreign decision의 세부를 재진술하지 않는다. ## 검증해야 할 주장 > 공식 문서 근거가 있어도 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | 대상 Keycloak Capability config의 `PKCE method`를 비워두면 server-side PKCE 강제가 활성화되지 않아 `plain` 또는 PKCE 없는 요청도 허용되는지 | `KC-PKCE-C1`/`C3`은 UI 라벨과 S256 선택 시 동작을 설명하지만 미설정 default와 거부 응답을 직접 증명하지 않음. 버전별 설정 key 차이도 가능 | dev realm에서 `PKCE method`를 비운 경우와 `S256`인 경우를 각각 export해 JSON을 대조하고, verifier 없는 token 교환의 응답을 확인 | `needs-confirmation` | | Keycloak 의 authorization code TTL default = 60s | 본 branch 의 Sources 에 Keycloak code TTL default 명세 없음 (본문 메모만) | dev Keycloak realm settings > Tokens > Authorization Code Lifespan 캡처 | `needs-confirmation` | | Authorization code 1회용 정책 위반 시 (재사용) Keycloak 이 두 번째 요청 거부 + **이미 발급된 토큰 invalidate** | 본 branch 의 Sources 는 code 재사용 시 토큰 revoke 동작을 다루지 않음. RFC 6749 §4.1.2 는 본 branch Sources 표에 미링크 (메모만 언급) | dev 환경에서 같은 code 로 `/token` 2회 호출 후 첫 번째 token 으로 보호 API 호출 → 401 확인 | `needs-confirmation` | | `state` 파라미터 검증 누락 시 실제로 CSRF 공격으로 공격자 code 주입 가능 | OA21-C5 (redirect URI exact match) 는 다른 방어. state 검증 자체의 RFC 권고는 본 branch Sources 의 verbatim 인용 범위 밖 (OAuth 2.1 §4.1 등 별도 인용 필요) | RFC 6749 §10.12 또는 OAuth 2.1 §4.1.1 의 state 권고 verbatim 인용 추가 수집 | `needs-confirmation` | | OAuth 2.1 §7.5.1 의 PKCE 강제 예외 조건이 본 P2A 시나리오에 적용되지 않는다 (즉 PKCE 가 무조건 MUST) | OA21-C1 의 "Does not prove" 가 §7.5.1 예외의 정확한 조건 미명시를 인정 | RFC 9700 (OAuth 2.0 Security BCP) 또는 OAuth 2.1 §7.5.1 verbatim 발췌 후 P2A SPA public client 시나리오 매핑 | `needs-confirmation` | ## 마주친 문제 - 이슈 1: `code_challenge_method`가 Keycloak 서버 측 client 설정에 강제되지 않으면 클라이언트가 `plain`을 보낼 위험. - 원인 가설: Keycloak client의 `PKCE method` 미설정 시 server-side enforcement가 비활성일 수 있음(실측 전 단정 금지) - 시도: (구현 없음, 문서 확인만) - 해결: client 설정에 `S256` 강제 — `documented-only` ## 묶음 - [[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]] > 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화. ### 오류 기록 (이 sub-sub-branch 작업 중 발생) - (없음 — 현재 documented-only 단계) ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - (없음 — Phase 3 실 구현 단계에 누적) ## 관련 일일 노트 ## 완료 후 정리 > 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 wiki/projects/에 추출. - PR 링크: (미구현 — 문서까지만) - 리뷰 메모: - 머지 결과 / 배포 환경: 없음 (P2A는 `documented-only` 범위) - **wiki 추출 대상**: 현 단계 없음. PKCE 자체는 `wiki/concepts/oauth2-pkce.md`로 합성 가능하나 6 패턴 비교 완성 이후 검토. - **추출하지 않을 항목**: P2A 구현 없음. `documented-only` 유지.