18 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-pkce-flow-stages (PKCE 4단계 — verifier/challenge/auth/exchange) | branch-note | raw | BR-KEYCLOAK-CHILD-B7701136 | branch-child | keycloak-patterns-overview | WI-KEYCLOAK-PATTERNS-OVERVIEW-020 |
|
1 | feature-keycloak-pkce-flow-stages | feature-keycloak-patterns |
|
|
2026-05-25 | in-progress | 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 무력화)
- 형식: 43~128 chars, unreserved =
- Step 2: code_challenge 계산 — 등급:
documented-onlycode_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))(S256)S256vsplain: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=<auth_code>&state=<echo>
- 추가 파라미터:
- Step 4: Token Request (
/tokenexchange) — 등급: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-onlycodeTTL: 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_verifierlength 43은 base64url(32 bytes) 결과 길이와 일치. 보통 32 random bytes로 생성하면 OK.- 대상 Keycloak UI에서는 Capability config의
PKCE method를S256으로 지정한다. 버전별 UI 차이는 생성된 realm export의 client 설정과 함께 대조하며, 미설정 시 실제 허용 동작은 실측 전까지 단정하지 않는다. staterandom 값은 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/<category>/<slug>.md#<CLAIM-ID>형식.
| 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
- 원인 가설: Keycloak client의
묶음
- 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유지.