Files
llm-wiki/raw/branch-notes/feature-keycloak-pkce-flow-stages.md
T

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
DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1
1 feature-keycloak-pkce-flow-stages feature-keycloak-patterns
keycloak-patterns
branch
keycloak-patterns
p2a
pkce
oauth2
rfc-7636
spa
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)
  • S256plain의 차이는? 왜 OAuth 2.1은 S256을 강제하는가?
  • state / nonce는 PKCE와 어떻게 다른 역할인가?
  • code_verifier가 localStorage에 노출되면 PKCE는 어떤 의미인가? (= 거의 무의미)

본 sub-sub-branch는 각 단계의 입력/출력/공격 모델/방어 효과를 한 줄씩 정리한다.

  • 이슈: (학습 노트, 이슈 없음)
  • PR: (구현 없음)

범위

포함 범위

  • (본문 해당 섹션에서 다룬 항목 참조)

제외 범위

  • (명시 필요)

근거 (필수, 최소 1개+)

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=<auth_code>&state=<echo>
  • 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 methodS256으로 지정한다. 버전별 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 Claimsraw/<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만 구현 근거로 사용한다.

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

묶음

본 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 유지.