The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
6.1 KiB
Markdown
107 lines
6.1 KiB
Markdown
---
|
|
id: 9ae4ec71-a32e-49a7-88c2-f7368541c28d
|
|
kind: QUESTION
|
|
slug: refresh-rotation-replica-contention
|
|
title: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
version: 33
|
|
questionStatus: OPEN
|
|
studio: "https://hyeonworks.com/studio/documents/9ae4ec71-a32e-49a7-88c2-f7368541c28d/edit"
|
|
public: "https://hyeonworks.com/questions/refresh-rotation-replica-contention"
|
|
---
|
|
|
|
# Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
|
|
|
|
realm이 refresh token rotation과 재사용 허용 0회를 쓰고 있다.
|
|
두 replica가 같은 refresh token으로 동시에 갱신할 수 있고, 그때 두 번째 사용이 거부될 가능성이 있다.
|
|
실제 Keycloak 응답과 session 영향은 아직 재현해 보지 않았다.
|
|
|
|
## 관계
|
|
|
|
- **BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가**
|
|
저장소 결정이 이 질문보다 앞선다.
|
|
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
|
rotation과 재사용 0회를 쓰는 구성의 출처다.
|
|
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
|
|
다중 인스턴스 운영이 이 경쟁의 전제다.
|
|
- **BFF 인증 구조 설계 기준**
|
|
갱신 실패를 화면 오류로 바꾸는 규칙이 이 기준의 항목이다.
|
|
|
|
## 사실
|
|
|
|
- realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱신하면 이전 refresh token은 바로 무효가 된다.
|
|
- 커밋된 테스트는 새 refresh token 발급과 이전 token 거부, revocation 뒤 refresh 실패를 확인한다.
|
|
- authorized client manager에는 refresh-token provider가 구성되어 있어 access token 만료 시 refresh를 시도할 수 있다.
|
|
- 만료를 기다려 실제 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
|
|
- 현재 authorized client 저장소는 process-local이라 replica가 같은 refresh token 상태를 공유하지 않는다.
|
|
따라서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
|
|
- 이미 발급된 access token은 만료 전까지 API에서 계속 통하기 때문에, 갱신이 실패해도 그동안은 화면이 정상으로 보이게 된다.
|
|
|
|
## 가정
|
|
|
|
- 운영에서는 replica가 둘 이상이고 저장소를 공유해 같은 authorized client 항목을 보게 된다.
|
|
- 두 replica가 비슷한 시각에 만료를 만나면 각각 갱신을 시도하게 된다.
|
|
|
|
## 미지수
|
|
|
|
- 같은 refresh token으로 두 replica가 동시에 갱신하면 각 replica가 어떻게 동작하게 되는지.
|
|
- 재사용 허용 0회에서 요청이 사용자 화면에 어떻게 보이게 되는지.
|
|
로그인 만료로 보이는가 일시적 오류로 보이는지.
|
|
- 저장소에서 새 token을 다시 읽어 재시도하면 성공하게 되는지, 아니면 재인증이 필요해지는지.
|
|
- 갱신을 한 곳에서만 할 것인지, 각자 하게 두고 실패는 재시도로 처리할 것인지.
|
|
- lock을 쓴다면 어디에 두고 얼마나 잡을지. 잡은 채로 프로세스가 내려가면 어떻게 푸는지.
|
|
- 갱신 실패를 로그인 만료와 구분해서 표시할 수 있는지.
|
|
|
|
## 제약
|
|
|
|
- rotation과 재사용 0회는 이미 realm에 설정한 상태다. 이 전제는 바꾸지 않고 답한다.
|
|
- 이미 발급된 access token은 만료 전까지 사용할 수 있으므로 refresh 실패는 즉시 보이지 않을 수 있다.
|
|
재현 테스트는 access token 만료 직후에 맞춰 실행한다.
|
|
- 이 경쟁은 저장소를 공유한 뒤에야 재현되므로 저장소를 정한 다음에 이어서 푼다.
|
|
|
|
## 선택지
|
|
|
|
### 1. 분산 lock으로 갱신을 직렬화한다
|
|
|
|
한 replica만 갱신하고 나머지는 끝나기를 기다렸다가 결과를 읽는 구성이다. 같은 refresh token을 두 replica가 동시에 쓰는 상황 자체를 만들지 않는 것이 목표다.
|
|
|
|
분산 lock을 사용하면 refresh 구간을 직렬화할 수 있다.
|
|
lock 저장소의 가용성, lock 만료, 재진입, lock 보유 process 종료 상황까지 함께 처리해야 한다.
|
|
|
|
### 2. 각자 갱신하고 실패는 재시도로 처리한다
|
|
|
|
구현이 가장 단순하다. 지는 쪽이 거부를 받으면 저장소에서 최신 token을 다시 읽어 재시도한다는 전제인데,
|
|
이 재시도가 성립하는지는 확인이 필요하다.
|
|
|
|
reuse detection 정책에 따라 같은 refresh token의 두 번째 사용이 token family 전체에 영향을 줄 수 있다.
|
|
이 경우 단순 retry로 끝나지 않고 재인증이 필요할 수 있다.
|
|
갱신에 성공한 replica가 새 token을 저장하기 전에 다른 replica가 다시 조회하는 순서도 별도로 확인해야 한다.
|
|
|
|
### 3. 갱신 전용 경로를 하나 둔다
|
|
|
|
refresh를 전담하는 구성요소 하나만 refresh token을 사용하고 다른 replica는 갱신 결과를 조회하도록 구성할 수 있다.
|
|
|
|
refresh를 전담하는 구성요소가 중단되면 access token 만료 이후 갱신을 수행할 주체가 없어지므로 해당 구성요소의 가용성과 복구 방식이 중요해진다.
|
|
|
|
### 4. 제약상 제외 — 재사용 허용을 늘린다
|
|
|
|
재사용을 짧게 허용하면 두 번째 사용이 거부되지 않고 코드도 고치지 않는다.
|
|
다만 rotation과 재사용 0회는 이미 realm에 설정한 상태이고,
|
|
훔친 refresh token을 그 시간 안에 쓸 수 있다는 문제도 남는다.
|
|
|
|
## 다음 검증
|
|
|
|
저장소를 공유한 뒤에 재현한다.
|
|
|
|
1. replica 두 대에서 같은 사용자로 access token 만료 직후 동시에 요청을 보낸다.
|
|
2. 이긴 쪽과 지는 쪽의 응답을 각각 기록한다.
|
|
3. 지는 쪽이 저장된 새 token으로 재시도해 성공하는지 본다.
|
|
4. 지는 쪽 사용자 화면에 무엇이 보이는지 기록한다.
|
|
5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다.
|
|
|
|
실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
|
|
rotation과 재사용 0회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다.
|