--- title: branch / feature-keycloak-refresh-token-rotation (Refresh token rotation + revocation) source_type: branch-note status: raw id: BR-KEYCLOAK-CHILD-579E54CC kind: branch-child project: keycloak-patterns-overview work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-007 inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1, DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1] refines: [] overrides: [] depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-004] contract_packet: 1 branch: feature-keycloak-refresh-token-rotation parent_branch: feature-keycloak-refresh-rotation-and-logout related_projects: [keycloak-patterns] tags: [branch, keycloak-patterns, p2a, refresh-token, rotation, revocation, keycloak] created: 2026-05-25 target_merge: status_label: in-progress contract_packet_sha256: 41b3c869ae7eecc249938a19e9501c8b8cecdb11d589882f2babb6d10f63618a --- # branch: feature-keycloak-refresh-token-rotation — Refresh token rotation + revocation > Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] Work Item branch의 child. > **목적**: refresh token rotation의 개념·정책·검증 계약 owner로서, 공식 확인된 `Revoke Refresh Token` 동작과 target-version 실험이 필요한 `Refresh Token Max Reuse`/reuse 결과를 구분한다. `/revoke` 계약과 JWT stateless 한계도 함께 정리한다. > `status_label`: `in-progress` | `review` | `merged` | `abandoned` ## 부모 (필수) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: refresh rotation과 logout 후 session·token 무효화가 검증된다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1` | AP1은 public client와 Authorization Code + PKCE를 사용한다 | public SPA의 refresh token rotation·revocation 계약에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] | | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1` | done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | parent의 rotation·logout 검증을 위한 개념·실험 계약에 적용한다 | [[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가 refresh_token을 보유하므로 탈취 시 공격자가 access_token을 계속 갱신할 수 있다. 공식 자료로 확인된 rotation 계약은 **사용된 refresh token을 무효화하고 새 refresh token을 발급한다**는 범위까지다(`KC-RTROT-C1`/`C2`). 이미 사용한 토큰을 다시 제출했을 때 후속 토큰까지 무효화되는지, 그 범위가 token family 전체인지, `Max Reuse` 값별 의미가 무엇인지는 target Keycloak 버전의 실행 실험으로만 확정한다. 핵심 질문: - Keycloak에서 rotation을 켜는 정확한 설정 항목과 위치는? - 이미 사용한 refresh token을 다시 제출하면 어떤 토큰·세션이 무효화되는가? (`Max Reuse=0`과 `1` 비교 관찰) - `/protocol/openid-connect/revoke` endpoint 사용 방법? - logout 시 access_token / refresh_token / session을 어떻게 정리? - 함정: **JWT access_token은 stateless** — revoke를 호출해도 만료까지 검증을 통과한다. 즉시성 확보 방법은? 본 sub-sub-branch는 **Keycloak Realm Settings 경로 + rotation flow + revocation endpoint + logout 정리 + stateless 한계**를 정리. - 이슈: (학습 노트, 이슈 없음) - PR: (구현 없음) ## 범위 ### 포함 범위 > 본 sub-sub-branch 는 **P2A 개념·계약 정리(`documented-only`)**. "무엇이 어떻게 동작하는가 + 어떤 설정/파라미터 계약인가" 까지만 다루고, 실제 docker-compose 시연·실측은 cousin(다른 phase) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] (P3A) 에 위임한다. - Keycloak refresh token rotation 설정의 **개념·계약**: `Revoke Refresh Token` 토글 동작 + rotation flow(RT 1회 사용 후 무효화·새 토큰 발급) — `KC-RTROT-C1`/`C2` - reuse detection 결과에 대한 **가설·검증 계약** — 후속 RT 유효성, 세션 상태, `Max Reuse=0/1` 차이를 P3A 실행 문서에서 관찰하며 family invalidation을 선결 사실로 두지 않음 - `/protocol/openid-connect/revoke` endpoint 의 **RFC 7009 request/response 계약** (`token`/`token_type_hint`, refresh↔access 무효화 SHOULD) — `RFC7009-C1`~`C4` - JWT stateless access token 의 **revoke 즉시성 한계** + 대응 옵션(짧은 TTL / introspection / blacklist / opaque) 트레이드오프 — `RFC7009-C5`~`C7` - **RP-Initiated(front-channel) logout** 파라미터 계약(`id_token_hint`, `post_logout_redirect_uri`, Valid Post Logout Redirect URIs) — `KC-LOGOUT-C1`~`C7` - 짧은 access token TTL 로 revoke 즉시성을 완화하는 **설계 근거**(RFC 자신의 short-lived-token 대안) — `RFC7009-C6` ### 제외 범위 > 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거. - 실제 rotation/revoke/logout 의 docker-compose **시연·실측** — cousin(P3A) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] 소유 - backend 매 요청 **introspection** 호출(stateless 포기 패턴) — 한계만 인지, 채택 안 함 - distributed **token blacklist** 캐시(Redis 등) 운영 패턴 - Keycloak **custom SPI / event listener** - **back-channel logout 수신** backend 구현과 provider-trigger E2E — 현재 두 refresh note 모두 범위 밖. 필요 시 공식 spec·framework 근거를 갖춘 전용 branch를 새로 만들어야 하며 현재 endpoint 존재를 가정하지 않음 - **opaque / reference token** 으로의 전환(Keycloak 지원하나 본 학습 범위 외) ## 근거 (필수, 최소 1개+) - [[raw/official-docs/keycloak-securing-apps-overview-official]] — Keycloak Securing Apps overview (Tokens / revocation) - [[raw/official-docs/oauth-v2-1-draft-ietf]] — OAuth 2.1 (refresh_token rotation 권고) - [[raw/official-docs/oauth2-pkce-rfc-7636]] — PKCE (refresh_token 보안 맥락) - [[raw/official-docs/keycloak-refresh-token-rotation-sessions-official]] — Keycloak Server Administration Guide (§_timeouts + §_refresh_token_rotation). D1(`Revoke Refresh Token` 토글 존재·동작)과 D4(RT 1회 사용 후 invalidate) 를 **부분** 뒷받침. `Refresh Token Max Reuse` 필드와 "family invalidate" 메커니즘은 이 자료에서 확인되지 않음(KC-RTROT-C6) — D1/D4 의 `UNSUPPORTED_DECISION` 라벨은 유지 필요. - [[raw/official-docs/oauth2-token-revocation-rfc-7009]] — RFC 7009 Token Revocation (revoke endpoint 표준 request/response 계약 + self-contained/JWT access token 의 revoke 즉시성 한계의 표준 근거, `RFC7009-C1`~`C6`) - [[raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official]] — Keycloak Server Admin Guide "Tokens tab" — "Revoke Refresh Token" 설정 공식 정의 확보 + "Refresh Token Max Reuse"/family-invalidate 동작의 verbatim **부재**를 전수 검색으로 확인(negative finding). D1/D4는 여전히 `UNSUPPORTED_DECISION` 유지. ⚠️ 위 `keycloak-refresh-token-rotation-sessions-official` 와 **동일 소스(server_admin Tokens 탭)의 중복 발췌** — 병합/아카이브는 사용자 판단(§보고 참조) - [[raw/official-docs/keycloak-oidc-logout-endpoint-official]] — Keycloak RP-Initiated(front-channel) logout 파라미터 계약(`end_session_endpoint`, `id_token_hint`, `post_logout_redirect_uri`, Backchannel Logout URL). D3 + 구현 가이드 §3 근거 (`KC-LOGOUT-C1`~`C7`) - [[raw/official-docs/oauth2-browser-based-apps-ietf-draft]] — OAuth 2.0 for Browser-Based Apps (IETF BCP). browser-based OAuth client = public client 가 토큰을 브라우저에 보유 → 탈취 위협 배경(D1 `선택 조건`, `OAUTH-BBA-C3`) ## TODO 각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation` - [ ] **Keycloak Realm Settings → Tokens 탭 항목** — 등급: `documented-only` - `Revoke Refresh Token`: **ON** — refresh 사용 후 해당 토큰 무효화 - `Refresh Token Max Reuse`: target UI에서 필드 존재를 확인한 뒤 **0과 1을 실험 입력값으로 비교**. 사전에 각 값의 의미를 부여하지 않음 - `SSO Session Idle`: 짧게 (예: 30분) — 일정 시간 미사용 시 세션 만료 - `SSO Session Max`: 강제 만료 시간 (예: 10h) - `Access Token Lifespan`: 5~15분 (짧을수록 revoke 즉시성 향상) - `Client Session Idle` / `Client Session Max`: client별 override - [ ] **Refresh Token Rotation flow** — 등급: `documented-only` ```text 1) SPA가 RT_1으로 /token (grant_type=refresh_token) 호출 2) Keycloak: RT_1 검증 → invalidate → AT_2 + RT_2 반환 3) SPA가 RT_2로 다음 갱신 → RT_2 invalidate → AT_3 + RT_3 ``` - [ ] **Reuse Detection 결과 가설 검증** — 등급: `needs-confirmation` - 공격자가 RT_1을 탈취하고 사용 → AT_2 + RT_2 받음 - 정상 사용자가 (모르고) RT_1을 다시 사용 → RT_1 응답과 RT_2의 후속 사용 결과, realm session 상태를 각각 관찰 - `Max Reuse=0`과 `1`에서 같은 sequence를 실행해 후속 토큰 무효화 범위를 기록. family 전체 invalidation은 가능한 관찰 결과 중 하나일 뿐 기대값으로 고정하지 않음 - [ ] **Revoke endpoint 사용법** — 등급: `documented-only` ```text POST /realms//protocol/openid-connect/revoke token= token_type_hint=refresh_token (또는 access_token) client_id= ``` - `token`/`token_type_hint` 파라미터 계약은 RFC 7009 §2.1 표준과 일치 — [[raw/official-docs/oauth2-token-revocation-rfc-7009]] `RFC7009-C3` (Keycloak 이 이 endpoint 를 실제로 RFC 7009 로 문서화하는지는 `RFC7009-C1` 의 "Does not prove" 참조 — 별도 vendor 확인 필요) - refresh_token revoke: RFC 상 SHOULD 로 관련 access token 도 함께 무효화될 수 있음(`RFC7009-C4`) — MUST 아님, AS 지원 여부에 달림 - access_token revoke: Keycloak은 introspection 시 invalid 응답, 그러나 **JWT를 stateless로 검증하는 backend는 모름** (`RFC7009-C5`/`C7`, 아래 함정 참조) - [ ] **Logout 시 토큰 정리** — 등급: `documented-only` - (a) Front-channel logout: `/protocol/openid-connect/logout?post_logout_redirect_uri=...&id_token_hint=` — 브라우저 redirect로 Keycloak 세션 종료 - (b) Back-channel logout: Keycloak client 설정 필드의 존재만 기록. 수신 endpoint와 provider-trigger E2E는 현재 범위에 없고 구현을 가정하지 않음 - (c) Refresh token revoke: 명시적으로 `/revoke` 호출 - SPA가 메모리에서 토큰 삭제 + cookie clear도 추가 - [ ] **함정: JWT access_token stateless 한계** — 등급: `documented-only` - JWT는 자체 서명 검증으로 valid 여부 판단 → backend가 **revoke 사실을 모름** — [[raw/official-docs/oauth2-token-revocation-rfc-7009]] `RFC7009-C5` (self-contained access token 은 AS 와 추가 상호작용 없이 인가 판단)가 표준 근거 - access_token 만료(`exp`)까지 backend는 valid로 통과시킴 — `RFC7009-C5` (self-contained token 은 AS 상호작용 없이 검증) + `RFC7009-C4` (access token 무효화는 AS 가 지원할 때만 SHOULD, MUST 아님) - 대응 옵션: 1. **짧은 TTL** (5~15분) — 가장 일반적 2. **Token Introspection** (`/protocol/openid-connect/token/introspect`) — 매 요청마다 Keycloak에 질의 → stateless 이점 상실, 성능 저하 3. **Revocation list / blacklist** — backend가 revoked jti 목록 캐싱 (운영 복잡) 4. **Reference token** (opaque) — JWT 대신 opaque token + introspection (Keycloak 지원하나 본 학습 범위 외) - [ ] **함정 정리** — 등급: `documented-only` - `Refresh Token Max Reuse`의 0/양수 의미를 실험 없이 일반화하면 버전별 동작을 잘못 문서화할 수 있음 - logout 시 `id_token_hint` 누락하면 prompt 떠서 UX 저하 - rotation 활성화 후 SPA 코드가 옛 RT를 재사용하면 실패하거나 후속 RT/세션에 영향이 갈 수 있음 → 정확한 범위는 실행 결과로 기록 - back-channel receiver가 없는 현재 scope에서 backend cache 무효화를 보장한다고 쓰지 않음 ## 진행 중 메모 작업하며 떠오른 메모. 자유 형식. - Keycloak 25.x 기준 Realm Settings → Tokens 탭 UI 항목은 버전에 따라 라벨이 약간 달라질 수 있음. 실 구현(P3A) 시 정확한 라벨 재확인 필요. - "JWT는 revoke가 안 된다"는 표현은 정확히는 "Keycloak이 revoke를 알리지만 stateless backend가 그 사실을 가져오지 않으면 모른다"가 맞음. 짧은 TTL + rotation 조합으로 실용적 보안 확보. - Keycloak의 `Backchannel Logout URL` 설정 필드 존재와 실제 수신 구현은 별개다. 현재 문서들은 receiver endpoint를 구현·위임하지 않으며, 필요 시 전용 branch에서 spec/framework 지원부터 확인한다. ## 결정 사항 (decisions) > 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. - 2026-05-25 (수정 2026-07-18): `Revoke Refresh Token: ON`은 rotation 실험의 candidate profile로 유지한다. `Refresh Token Max Reuse: 0`은 1과 비교할 **실험 입력값**이며, 다른 값이 탐지를 약화시킨다는 의미는 target-version 결과 전에는 주장하지 않는다. - 2026-05-25: access_token revocation 즉시성은 **짧은 TTL(5~15분)**로 해결. introspection은 stateless 이점 상실 + 성능 저하로 학습 범위에서 권장 안 함. - 2026-07-18: back-channel logout receiver와 provider-trigger E2E는 P2A/P3A 두 refresh note 모두 범위 밖이다. 현재 cousin에 위임하지 않으며, 필요 시 전용 branch를 신설한다. ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. > 본 sub-sub-branch 는 `documented-only`. `Revoke Refresh Token` 토글의 **동작**은 이제 Keycloak 공식 doc 로 뒷받침되나(`KC-RTROT-C1`/`C2`), **`Refresh Token Max Reuse` 필드명**과 **"재사용 시 family 전체 invalidate"** 동작은 Keycloak 26.7.0 Server Admin Guide 전수 검색에서 verbatim 부재 확인(`KC-RTROT-C6`) → 해당 부분만 `UNSUPPORTED_DECISION` 유지, 실측은 P3A cousin 에 위임. > `선택 조건` 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`. | Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | **rotation 정책·검증 profile owner** — `Revoke Refresh Token: ON`을 candidate로 두고 `Refresh Token Max Reuse=0/1`을 비교 실험한다. 최종 값과 의미는 target-version 관찰 뒤 확정 | SPA(public client)가 refresh token을 브라우저에 보유하는 P2A/P3A 배치에서 rotation을 평가. BFF/token-mediating backend([[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]])에서는 위협 모델이 달라 재평가. 병렬 refresh가 필요한 client는 `suppress-refresh-token-rotation` executor 예외(`KC-RTROT-C3`) 검토 | `raw/official-docs/keycloak-refresh-token-rotation-sessions-official.md#KC-RTROT-C1`, `#KC-RTROT-C2`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3`, `raw/official-docs/oauth2-browser-based-apps-ietf-draft.md#OAUTH-BBA-C3`. `Max Reuse` 필드·값 의미·family 동작은 `UNSUPPORTED_DECISION`(`KC-RTROT-C6`) | `official-vendor-doc + official-standard + needs-confirmation (Max Reuse semantics)` | target UI/export에서 필드와 내부 key를 확인하고 0/1에서 동일 reuse sequence를 실행. family invalidation 여부는 결과값으로만 기록 | | D2 | access token revocation 즉시성은 짧은 TTL(5~15분) 로 해결, introspection 패턴은 stateless 이점 상실로 권장 안 함 | **stateless JWT 검증(Spring RS)을 유지**하는 한 → 짧은 TTL. "초 단위 즉시 무효화"가 hard requirement 면 → introspection 또는 opaque/reference token(stateless 포기 + 성능 비용) | `raw/official-docs/oauth2-token-revocation-rfc-7009.md#RFC7009-C5` (self-contained/JWT access token 은 AS 와 추가 상호작용 없이 검증 → revoke 즉시 반영 안 될 수 있음) + `#RFC7009-C6` (짧은 수명 access token 이 RFC 자신의 설계 대안) + `#RFC7009-C4` (access token 무효화는 AS 가 access token revocation 을 지원할 때만 SHOULD — 미지원/self-contained 시 즉시 무효화 안 됨). 방향성은 official-standard 근거 보유. **구체적 수치 "5~15분"** 은 RFC 가 분 단위를 제시 안 하므로 `UNSUPPORTED_IMPL_DECISION` (trade-off: 짧을수록 안전하나 refresh 왕복/서버 부하↑ — 5~15분은 임의 균형점) | `official-standard (방향성) + UNSUPPORTED_IMPL_DECISION (TTL 수치)` | Keycloak 이 access token revocation(RFC7009 §2 SHOULD)을 실제 지원하는지 확인 + OWASP/Keycloak 공식 권장 TTL 구간 raw 추가로 수치 보강 | | D3 | RP-Initiated logout 파라미터 계약까지만 소유. back-channel receiver 구현·provider-trigger E2E는 현재 scope 밖이며 endpoint를 가정하지 않음 | 현재 요구는 브라우저 logout과 revoke 계약 학습. backend cache 즉시 무효화가 별도 요구가 되면 전용 branch에서 OIDC Back-Channel Logout spec과 framework 지원을 확보한 뒤 설계 | `raw/official-docs/keycloak-oidc-logout-endpoint-official.md#KC-LOGOUT-C7` (설정 필드 존재만 증명) + `UNSUPPORTED_DECISION` (receiver 미설계) | `official-vendor-doc (field only) + scoped out` | receiver가 구현됐다는 인상을 주는 링크·예상 endpoint를 두지 않음 | | D4 | **reuse 결과 검증 계약** — RT_1 사용 후 무효화·AT_2+RT_2 발급까지는 공식 계약, RT_1 재사용 응답과 RT_2/realm session 상태는 관찰 항목 | D1 profile의 0/1 각각에 동일 sequence 적용. family 전체 invalidation은 가능한 결과 중 하나이며 expected fact가 아님 | `raw/official-docs/keycloak-refresh-token-rotation-sessions-official.md#KC-RTROT-C2` + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` + `UNSUPPORTED_DECISION`(`KC-RTROT-C6`) | `official-vendor-doc (초기 rotation) + needs-confirmation (reuse impact)` | P3A 실행 owner가 RT_1 재사용 status, RT_2 후속 status, session 상태를 분리 기록해야 함 | ## 구현 가이드 > *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세 — 다음 구현자(P3A cousin)가 되묻지 않아도 설정·호출을 작성할 수 있는 수준. 본 노트는 `documented-only` 이므로 각 항목은 **설정/파라미터 계약**까지이며, 실측 승격은 Claims To Verify + P3A cousin 소관. > > **3-rule**: (R1) 각 cell 은 Decision ID + Supporting Claim ID trace, (R2) 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄, (R3) 본 branch 결정 범위 밖은 남기지 않음. ### 1. Keycloak Realm Settings — rotation & timeout 설정 계약 > **Trace**: D1(`KC-RTROT-C1`/`C2`) + D2(`KC-RTROT-C4`) — Realm Settings → Sessions/Tokens 탭. > > - **UNSUPPORTED_IMPL_DECISION**: `Refresh Token Max Reuse` 필드명·0/1 의미·reuse 후 영향(`KC-RTROT-C6` doc text 부재 — admin UI/export와 실험 필요) / `Access Token Lifespan`의 "5~15분" 수치(RFC·Keycloak doc 모두 분 단위 미제시). | 설정 | 위치 (Realm Settings) | 값 | 근거 / 상태 | |---|---|---|---| | `Revoke Refresh Token` | Sessions/Tokens 탭 | **Enabled** | `KC-RTROT-C1` — documented | | `Refresh Token Max Reuse` | Tokens 탭 (노출 여부 포함 확인) | **0과 1을 각각 실험** | `UNSUPPORTED_IMPL_DECISION` — 필드·값 의미 doc text 부재(`KC-RTROT-C6`), UI/export + runtime 비교 | | `Access Token Lifespan` | Tokens 탭 | 5~15분 | `KC-RTROT-C4`(설정 존재) + `UNSUPPORTED_IMPL_DECISION`(수치) | | `SSO Session Idle` / `SSO Session Max` | Sessions 탭 | 프로젝트값(예: 30m / 10h) | `KC-RTROT-C4` — documented | | `Client Session Idle` / `Client Session Max` | Sessions 탭 | SSO 값보다 짧게(client override) | `KC-RTROT-C4` — documented | ### 2. Revoke endpoint 호출 계약 (RFC 7009) > **Trace**: D2 + `RFC7009-C1`~`C4`. > > - **UNSUPPORTED_IMPL_DECISION**: Keycloak 이 이 endpoint 를 *RFC 7009 로* 문서화/준수한다는 명시는 vendor 확인 필요(`RFC7009-C1` "Does not prove"). 여기서는 RFC 표준 계약 형태만 고정. ```text POST /realms//protocol/openid-connect/revoke token= # REQUIRED (RFC7009-C2) token_type_hint=refresh_token|access_token # OPTIONAL, 서버 조회 최적화용 (RFC7009-C3) client_id= # client 인증 ``` - refresh_token revoke → AS 가 access token revocation 을 지원하면 관련 access token 도 **SHOULD** 함께 무효화(`RFC7009-C4` — MUST 아님). - access_token revoke → Keycloak introspection 은 invalid 로 응답하나, JWT 를 stateless 로 검증하는 backend 는 그 사실을 모름(§4 참조). ### 3. Front-channel(RP-Initiated) logout 파라미터 계약 > **Trace**: `KC-LOGOUT-C1`~`C7` (In-scope logout; back-channel *수신* 은 D3 로 out of scope). | 요소 | 계약 | 근거 | |---|---|---| | endpoint | `/realms//protocol/openid-connect/logout` (= `end_session_endpoint`) | `KC-LOGOUT-C1`/`C2` | | `id_token_hint` | 없으면 로그아웃 confirm UI 가 뜰 수 있음 → UX 위해 전달 권장 | `KC-LOGOUT-C3` | | `post_logout_redirect_uri` | 제공 시 자동 redirect. 단 `client_id` 또는 `id_token_hint` **동반 필수** + client 의 `Valid Post Logout Redirect URIs` 와 매칭 필요 | `KC-LOGOUT-C4`/`C5`/`C6` | | `Backchannel Logout URL` (client 설정) | **필드 정의만** in-scope. receiver endpoint와 provider-trigger E2E는 현재 존재를 가정하지 않으며 별도 요구 시 전용 branch 필요 | `KC-LOGOUT-C7` | ### 4. Stateless JWT access token 즉시성 완화 config > **Trace**: D2 + `RFC7009-C5`/`C6`/`C7`. > > - **UNSUPPORTED_IMPL_DECISION**: TTL 수치(§1 과 동일 trade-off). - backend(Spring RS)는 서명 + `iss`/`aud`/`exp` 만 검증 → revoke 사실을 모름(`RFC7009-C5`). `exp` 만료까지 valid 통과(`RFC7009-C5` self-contained + `RFC7009-C4` 조건부 SHOULD). - 짧은 TTL = RFC 자신이 제시하는 설계 대안(`RFC7009-C6`). 채택. | 대응 옵션 | stateless 유지? | 비용 | 본 노트 판정 | |---|---|---|---| | 짧은 TTL (5~15분) | ✅ 유지 | revoke 후 최대 TTL 만큼 노출 창 | **채택** (`RFC7009-C6`) | | Token Introspection (매 요청) | ❌ 포기 | 매 요청 Keycloak 왕복·성능↓ | 한계만 인지, 미채택 | | Revocation list / jti blacklist | 부분 | backend 캐시 운영 복잡 | out of scope | | Opaque/reference token | ❌ 포기 | introspection 상시 | out of scope | ## 엣지·실패·의존 > R4 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. - **실패·엣지 경로**: - **RT 캐시 후 옛 RT 재사용** → RT_1 요청이 실패하고 후속 RT/세션에도 영향이 갈 수 있음. 정확한 범위는 D4 실험으로 관찰. 클라이언트는 in-flight refresh를 단일 진입점으로 직렬화한다. - **동시 silent renew race** → 동일 RT 동시 제출은 D4 관찰을 오염시킬 수 있음. 0/1 각 실험에서 단일 refresh 진입점을 보장하고 manual 시연 시 silent renew를 일시 중단한다. - **logout `id_token_hint` 누락** → confirm prompt 로 UX 저하(`KC-LOGOUT-C3`). 기대: id_token 보관 후 전달. - **`post_logout_redirect_uri` 미등록** → `Valid Post Logout Redirect URIs` 매칭 실패로 redirect 거부(`KC-LOGOUT-C6`). 기대: client 설정에 사전 등록. - **access_token revoke 직후 만료 전 호출** → 200 통과(stateless JWT, `RFC7009-C5`) — **함정(의도된 한계)**. 기대: `exp` 까지 유효, TTL 후 401. - **back-channel receiver 부재** → 현재 범위에서는 backend session/cache의 즉시 무효화를 보장하지 않는다. 필요 시 전용 branch를 생성한다. - **다른 계약 의존**: - 부모 [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] — P2A 배치(SPA-direct, no Google) 컨텍스트를 consume. 배치가 BFF 로 바뀌면 D1 전제(브라우저가 RT 보유) 붕괴. - [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] `D1`/`D4` — RT 를 브라우저 어디에 저장하느냐(그 브랜치 D1: AT 메모리 + RT httpOnly cookie)가 탈취 위험/rotation 필요성의 **전제**이며, 그 브랜치 D4 가 "refresh_token 은 rotation 에 의존" 을 명시. 저장 결정이 바뀌면 본 브랜치 위협모델 영향. - [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] `D1` — backend 가 `iss`+signature+`exp`+`aud` 4종 검증(그 브랜치 D1)이 D2 stateless 한계의 전제. `exp` 만료 시 401 동작이 §4 의 근거. - [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] — 최초 토큰(AT_1/RT_1) 발급 흐름(PKCE)을 consume — rotation 은 그 이후 단계. - cousin(다른 phase) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] (P3A) — 본 개념 계약의 **실측·시연** 소유. 본 노트 = 개념/계약, 그쪽 = 실행. ## 검증해야 할 주장 > 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | Keycloak Realm Settings → Tokens 탭에 `Revoke Refresh Token` 토글과 `Refresh Token Max Reuse` 입력이 정확히 그 라벨로 존재 | Keycloak 버전마다 admin UI 라벨이 달라질 수 있음; cited raw 에서 verbatim 미회수 | Keycloak 25.x docker 컨테이너 실행 후 admin UI 캡처 + Server Admin Guide raw source 발췌 추가 | `needs-confirmation` | | `Refresh Token Max Reuse=0`과 `1`에서 RT 재사용 결과가 어떻게 다른지(후속 RT·realm session 포함) | 필드·값 의미와 family invalidation 메커니즘이 cited raw에 verbatim 없음 | 각 값으로 realm을 재설정한 뒤 RT_1 사용→RT_2 발급→RT_1 재사용→RT_2 후속 사용→session 상태를 동일 순서로 기록 | `needs-confirmation` | | `/protocol/openid-connect/revoke` 엔드포인트가 RFC 7009 Token Revocation 을 준수한다 | RFC 7009 의 raw source 부재; Keycloak 의 RFC 준수 여부 verbatim 인용 없음 | RFC 7009 raw 발췌 후 Keycloak Server Admin Guide 의 "Token Revocation" 섹션과 cross-check | `needs-confirmation` | | JWT stateless backend 가 access_token revoke 사실을 모름 — 만료 전 검증 통과 | OAuth 2.1 / PKCE RFC 에 stateless JWT introspection 트레이드오프의 verbatim 인용 없음 | Spring Security Resource Server 로 JWT 검증 설정 후, revoke 직후 동일 token 으로 호출 → 200 응답 확인 (TTL 5분) | `planned` | | RP-Initiated logout 파라미터가 target Keycloak에서 문서 계약대로 동작하는지 | 공식 파라미터 계약은 있으나 runtime 미검증. back-channel receiver는 본 claim과 scope에 포함하지 않음 | front-channel logout만 실행해 session 종료·redirect를 확인. back-channel 요구가 생기면 전용 branch에서 별도 검증 | `needs-confirmation` | ## 마주친 문제 - 이슈 1: rotation 상태에서 SPA가 옛 refresh_token을 재시도하면 정상 사용자 흐름도 실패할 수 있음. - 원인 가설: reuse 처리 범위가 정상/공격 주체를 구분하지 않을 수 있음. 후속 RT·session 영향은 target-version 실험 전 확정하지 않음 - 시도: (구현 없음) - 해결: SPA가 refresh 진행 중에는 단일 진입점으로 직렬화 (in-flight refresh promise 공유) — `documented-only` ## 묶음 - [[raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official]] - [[raw/official-docs/keycloak-refresh-token-rotation-sessions-official]] - [[raw/official-docs/oauth-v2-1-draft-ietf]] - [[raw/official-docs/oauth2-token-revocation-rfc-7009]] - [[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 실 구현 단계에 누적) ## 관련 일일 노트 ## 완료 후 정리 > 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 wiki/projects/에 추출. - PR 링크: (미구현 — 문서까지만) - 리뷰 메모: - 머지 결과 / 배포 환경: 없음 (P2A는 `documented-only` 범위) - **wiki 추출 대상**: 현 단계 없음. 추후 `wiki/concepts/refresh-token-rotation-revocation.md`로 합성 후보 (다른 패턴과 공통). - **추출하지 않을 항목**: P2A 구현 없음. `documented-only` 유지.