334 lines
30 KiB
Markdown
334 lines
30 KiB
Markdown
---
|
|
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`
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
[[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: refresh rotation과 logout 후 session·token 무효화가 검증된다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| 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]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
|
|
없음.
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
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: (구현 없음)
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
> 본 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/<realm>/protocol/openid-connect/revoke
|
|
token=<token>
|
|
token_type_hint=refresh_token (또는 access_token)
|
|
client_id=<spa-client>
|
|
```
|
|
- `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=<id_token>` — 브라우저 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/<realm>/protocol/openid-connect/revoke
|
|
token=<token> # REQUIRED (RFC7009-C2)
|
|
token_type_hint=refresh_token|access_token # OPTIONAL, 서버 조회 최적화용 (RFC7009-C3)
|
|
client_id=<spa-client> # 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/<realm>/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`
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[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]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
> 본 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` 유지.
|