feat: 문서 구조 변경 및 tech-visual 스킬 추가

This commit is contained in:
DongHyeonka
2026-09-04 18:20:00 +09:00
parent 43901f0abf
commit 2efb7ee1f2
683 changed files with 61180 additions and 10479 deletions
@@ -0,0 +1,133 @@
---
id: 18a5cde2-dd1e-4bff-9f1c-997577ae438f
kind: QUESTION
slug: bff-session-authorized-client-store
title: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
topic: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 32
questionStatus: OPEN
studio: "https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit"
public: "https://hyeonworks.com/questions/bff-session-authorized-client-store"
---
# BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
Application Session과 Authorized Client는 저장하고 조회하는 기준이 서로 다르다.
Session은 `session ID`를 기준으로 조회하지만, Authorized Client는 `client registration 이름``principal name`을 기준으로 조회한다.
그래서 두 상태를 반드시 같은 저장소에 보관해야 하는 것은 아니며, 각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 결정해야 한다.
현재 Shared Store의 후보로는 Redis를 우선 생각하고 있지만 아직 최종 저장소로 결정한 것은 아니다.
특히 Access Token과 Refresh Token을 Redis에 저장할 경우 Token을 어떤 방식으로 암호화할지, Session과 Token의 만료 시간을 어떻게 맞출지, Logout할 때 Session과 Authorized Client가 모두 정상적으로 제거되는지까지는 확인하지 않았다.
따라서 현재 단계에서는 Redis를 저장소 후보로 작성하고, Token 보호와 만료 처리, Logout 시 상태 정리까지 검증한 뒤 실제 저장 구조를 결정한다.
## 관계
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
이 질문에서 저장소 부분만 떼어 낸 것이다.
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- **BFF 인증 구조 설계 기준**
이 기준의 저장소 항목이 이 질문의 답을 기다린다.
- **Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가**
저장소를 공유한 뒤에야 replica 경쟁이 재현된다.
## 사실
- 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이다.
- session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다.
두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
## 가정
- 두 상태를 같은 저장소에 둘 필요는 없다.
- 저장된 refresh token을 평문으로 두면 안 된다.
- session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
예를 들면 token이 만료됐을 때 사용자의 로그인 상태가 여전히 유지되는지.
## 미지수
- Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가.
요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- session과 authorized client를 같은 store에 둘지 나눌지.
- 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- session과 Authorized Client 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- sticky session이 durable store의 대안이 되는가 보완이 되는가.
## 제약
- authorized client의 조회 시 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token을 보게 된다.
- 현재 테스트에는 저장소 관련 계약이 없다.
- 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
## 선택지
### 1. session과 authorized client를 모두 Redis에 둔다
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 사용하면 여러 애플리케이션 인스턴스가 같은 Session과 Authorized Client 정보를 조회할 수 있다.
상태가 특정 인스턴스의 메모리에 묶이지 않으므로 서버가 재시작되거나 요청이 다른 Replica로 전달되는 환경에서도 인증 상태를 공유하기 쉬워진다.
Session과 Authorized Client에 설정된 유효 시간에 따라 Redis의 TTL을 이용해 저장된 상태를 만료시키는 구조도 구성할 수 있다.
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 Session 정책과 OAuth Token의 수명에 맞춰 별도로 정해야 한다.
이 구성에서는 인증 경로가 Redis의 가용성에 의존하게 된다.
Redis에 장애가 발생했을 때 기존 Session과 Authorized Client를 조회하지 못하는 상황을 어떻게 처리할지 정해야 하며,
장애 복구와 데이터 유지 방식도 함께 고려해야 한다.
또한 Access Token과 Refresh Token을 Redis에 저장한다면 저장된 Token을 어떤 방식으로 보호할지도 결정해야 한다.
Token을 암호화해서 저장할지, 암호화한다면 Key를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
### 2. session과 authorized client를 모두 JDBC에 둔다
JDBC 기반 저장소를 사용하면 이미 운영 중인 관계형 DB에 Session과 Authorized Client 정보를 저장할 수 있다.
인증 과정에서 Session이나 Authorized Client를 조회할 때마다 DB 접근이 발생하므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 영향을 받게 된다.
요청량이 증가했을 때 Session 조회가 지연되지 않는지 확인하고, 인증 관련 조회가 기존 애플리케이션 쿼리와 서로 영향을 주지 않는지도 확인해야 한다.
또한 만료된 Session과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기를 정해야 한다.
JDBC를 고르면 기존 DB 운영 체계를 그대로 쓰면서 조회 지연과 DB 부하, 만료 데이터 정리까지 같이 관리하게 된다.
### 3. session만 공유하고 sticky session을 쓴다
Session Affinity를 사용하면 같은 사용자의 요청을 가능한 한 동일한 애플리케이션 인스턴스로 전달할 수 있으므로 기존 구조의 변경을 줄일 수 있다.
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되는 것은 아니다.
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 저장되어 있다면, 요청이 다른 인스턴스로 전달되거나 해당 인스턴스가 종료되었을 때 기존 Token 정보를 조회할 수 없다.
Session Affinity는 요청을 특정 인스턴스로 보내는 방법이다. Session과 Authorized Client를 공유하는 문제와 장애 이후에 인증 상태를 유지하는 문제는 그대로 남는다.
인스턴스 장애와 Replica 간 이동까지 고려한다면 Session과 Authorized Client의 저장 방식을 별도로 설계해야 한다.
### 4. session은 Redis, token은 암호화한 JDBC에 둔다
Session과 Authorized Client를 서로 다른 저장소에 보관하는 방법도 있다.
요청마다 자주 조회되는 Session은 Redis와 같이 빠르게 접근할 수 있는 저장소에 두고, Access Token과 Refresh Token은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 저장할 수 있다.
각 데이터의 접근 패턴과 보호 요구사항에 맞춰 저장소를 선택할 수 있다는 장점이 있다.
두 저장소의 상태는 함께 관리해야 한다.
Session이 만료되었는데 Authorized Client가 남거나, 반대로 Authorized Client가 먼저 제거되어 유효한 Session에서 Token을 찾지 못하는 상황이 발생할 수 있다.
따라서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
Logout에서도 Session과 Authorized Client가 서로 다른 저장소에 있으므로 두 상태를 모두 정리해야 한다.
한쪽을 제거하는 과정에서 실패했을 때 어떻게 처리할지도 함께 결정해야 한다.
또한 Redis와 관계형 DB를 모두 인증 경로에서 사용하게 되므로 모니터링, 장애 대응, 백업 등 운영해야 하는 저장소도 늘어난다.
저장소를 나눠서 얻는 것과 늘어나는 운영 대상을 같이 놓고 정한다.
## 다음 검증
후보마다 같은 입력으로 비교한다.
1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.
3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.
5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
@@ -0,0 +1,124 @@
---
id: 7ff40767-a00b-4db2-98f6-0cdfce8c8936
kind: QUESTION
slug: edge-authorization-scope
title: Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가
topic: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 36
questionStatus: OPEN
studio: "https://hyeonworks.com/studio/documents/7ff40767-a00b-4db2-98f6-0cdfce8c8936/edit"
public: "https://hyeonworks.com/questions/edge-authorization-scope"
---
# Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가
현재 Edge는 인증된 사용자의 `user``email`만 Header로 전달하고 있으며, Upstream 애플리케이션에서는 Role을 이용한 인가 판단을 하지 않는다.
따라서 현재 구조만으로는 Role 기반 인가가 필요한 요구사항이 추가되었을 때 어떻게 처리할지 결정되어 있지 않다.
Edge가 사용자의 Role까지 확인해 Header로 전달할지, 아니면 애플리케이션이 Role과 권한을 확인하고 인가를 직접 판단하도록 할지 별도로 결정해야 한다.
Role이나 권한처럼 애플리케이션의 기능과 밀접한 정보가 계속 늘어난다면, 이러한 정보를 Edge Header에 계속 추가하기보다 인가 책임을 애플리케이션에서 처리하는 구조가 더 적절한지도 함께 검토한다.
## 관계
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
edge가 user와 email만 전달한다는 사실의 출처다.
- **Forward-Auth에서 Identity Header를 신뢰하기 위한 조건**
헤더 allowlist와 검증 조건이 이 기준에 있다.
- **BFF 인증 구조 설계 기준**
되돌리는 선택지의 기준이 이 문서다.
## 사실
- 지금 edge 응답은 user와 email만 전달한다. role과 groups, tenant, 인증 방식, token 만료는 전달하지 않는다.
- upstream의 identity endpoint는 role을 확인하지 않는다. 누가 왔는지만 응답한다.
- 현재 Internal Token 검증은 특정 Controller에서만 수행하고 있으며, Security 설정에서는 해당 경로를 `permitAll`로 허용하고 있다.
이 구조에서는 같은 내부 경로 아래에 새로운 Endpoint를 추가하더라도 Internal Token 검증이 자동으로 적용되지 않는다.
그래서 Filter, Interceptor, 또는 Spring Security의 인증 처리 단계처럼 공통 경계에서 검증하도록 옮겨야 한다.
- Nginx는 client가 보낸 동명 헤더를 merge하지 않고 덮어쓴다. 추가적으로 늘어나는 헤더도 같은 처리를 받아야 한다.
- upstream은 JWT를 입력으로 받지 않아서 헤더로 넘어온 값을 검증할 방법이 없다.
## 가정
- 헤더 종류가 늘어나면 정해야 할 계약도 늘어난다.
- role이 바뀌는 시점과 요청이 오는 시점이 달라서 그 사이에 들어온 요청은 바뀌기 전의 값을 볼 수도 있다.
## 미지수
- 다중 값 role을 어떤 구분자와 escaping으로 보낼지. 값 안에 그 구분자가 들어오면 어떻게 되는지.
- 헤더 크기 상한을 넘으면 어떻게 되는지. proxy가 자르는지 요청 자체가 거부되는지.
- role이 바뀌었을 때 proxy session과 downstream 인가가 언제 반영되는지. 권한 변경이 몇 분 뒤에 반영되는지.
- upstream이 헤더 존재만 볼지 값과 service identity까지 볼지.
## 제약
- 전달할 헤더는 allowlist로 해야 하고 client가 보낸 동명 헤더는 항상 덮어써야 한다.
- internal token 검사가 controller 한 곳에만 있다. 헤더를 늘리기 전에 이 검사를 공통 경계로 옮겨야 된다.
## 선택지
### 1. 인증만 edge에 둔다
Edge가 전달하는 Header를 `user``email` 정도로 제한하면 Edge와 Upstream 사이의 계약을 작게 유지할 수 있다.
Role이나 Permission 정보를 Header에 계속 추가하지 않으므로 Header 크기가 커지는 문제도 줄일 수 있다.
이 경우 인가 판단은 각 Upstream 애플리케이션이 직접 수행한다.
애플리케이션은 전달받은 사용자 식별 정보를 기준으로 자신의 저장소에서 Role이나 Permission을 조회하고,
해당 요청을 허용할지 결정해야 한다.
이 구조에서는 서비스마다 권한 조회와 인가 로직을 별도로 구성해야 한다.
따라서 Edge의 책임은 단순하게 유지할 수 있지만, 서비스 수가 늘어나면 각 서비스에서 동일하거나 유사한 권한 조회 체계를 반복해서 구현하고 운영해야 할 수 있다.
### 2. role 전달까지 edge에 둔다
Edge가 공통 Role 정보를 확인해 Upstream에 전달하면 각 서비스가 별도로 사용자 권한을 조회해야 하는 작업을 줄일 수 있다.
대신 Role을 Header로 전달하기 위한 계약을 먼저 정해야 한다.
사용자가 여러 Role을 가질 때 어떤 형식으로 직렬화할지, Header에 허용할 최대 크기를 어디까지로 할지, 사용자의 Role이 변경되었을 때 언제부터 새로운 값이 요청에 반영되는지도 명확하게 정의해야 한다.
또한 Upstream은 전달받은 Role이 원래 인증 시스템의 값과 일치하는지 확인할 수 있어야 하고, 그러지 못하면 Edge가 전달한 값을 그대로 신뢰하게 된다. 따라서 Edge에서 Role을 잘못 계산하거나 오래된 값을 전달하면 Upstream의 인가 판단도 그대로 잘못될 수 있다.
이 구조를 선택하게 되면 Edge가 Role 정보를 만드는 과정과 Header를 전달하는 경로를 신뢰 경계의 일부로 보고,
Role 갱신과 전달 오류를 어떻게 검증할지도 함께 설계해야 한다.
### 3. tenant와 인가 판단까지 edge에 둔다
Tenant 정보는 단순한 사용자 속성이 아니라 어떤 조직의 데이터에 접근할 수 있는지를 결정하는 값이다.
따라서 잘못된 Tenant 값 하나가 전달되면 다른 조직의 데이터에 접근하는 문제로 바로 이어질 수 있다.
이 때문에 Tenant를 Edge Header로 전달하려면 Upstream에서도 해당 사용자가 실제로 그 Tenant에 속하는지 다시 확인할 수 있는 방법이 필요하다.
현재 구성에는 이러한 재검증 수단이 없으므로 Tenant까지 Edge가 책임지는 구조로 확장하기에는 위험이 크다.
더 나아가 Tenant나 Role을 이용한 실제 인가 판단까지 Edge로 옮기면 Edge가 애플리케이션의 도메인 규칙을 알아야 한다.
어떤 사용자가 어떤 조직의 어떤 기능을 사용할 수 있는지 같은 정책이 바뀔 때마다 Edge의 로직도 함께 수정하고 배포해야 한다.
따라서 Edge는 인증된 사용자 정보를 전달하는 역할에 가깝게 유지하고, Tenant 소속 관계나 도메인에 종속된 인가 규칙은 Upstream 애플리케이션에서 검증하는 구조를 우선 검토한다.
### 4. 헤더 계약 대신 BFF가 인가와 API 호출을 맡는다
Role이나 Tenant 같은 정보를 Edge Header에 계속 추가하는 대신, BFF가 사용자에게 필요한 정보를 직접 조회하고 인가 판단과 API 호출을 처리하는 구조도 선택할 수 있다.
이 구조에서는 Edge가 애플리케이션의 Role, Tenant, 권한 정책까지 알 필요가 없다.
BFF가 필요한 사용자와 권한 정보를 조회해 인가를 판단하고, 화면에 필요한 여러 Resource Server의 API를 호출해 결과를 조합할 수 있다. 따라서 애플리케이션 도메인에 가까운 책임을 Edge Header 계약에서 분리할 수 있다.
이 구조에서는 BFF를 도입하면서 서버가 다시 인증 상태를 관리해야 한다.
브라우저와 BFF 사이의 Application Session을 보호해야 하고, Cookie 기반 Session을 사용한다면 상태 변경 요청에 대한 CSRF 검증도 필요하다.
여러 BFF Replica에서 인증 상태를 유지해야 한다면 Session과 Authorized Client를 어떻게 공유할지 결정하고 Shared Store의 장애와 만료 처리도 운영해야 한다.
## 다음 검증
upstream이 실제로 요구하는 claim을 먼저 적는다.
1. 전달하려는 claim이 계속 늘어나는가.
2. role이나 tenant 변경이 즉시 반영돼야 하는가.
3. 정책이 애플리케이션 도메인을 알아야 하는가.
4. 헤더 값이 인가 판단의 근거가 되는가.
5. 서비스별 정책 차이가 커지는가.
2번부터 5번 중 하나라도 그렇다면 헤더를 늘리는 대신 BFF 구조를 검토한다.
role을 헤더에 담는 구성을 먼저 만들고, 다중 값과 크기 상한을 넣어 무엇이 먼저 잘못되는지 확인한다.
role을 바꾼 뒤 몇 번째 요청부터 반영되는지도 확인한다.
@@ -0,0 +1,131 @@
---
id: c72656b5-842d-45d9-b5f6-82b66b09d0b9
kind: QUESTION
slug: server-session-pattern-multi-instance
title: 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가
topic: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 39
questionStatus: OPEN
studio: "https://hyeonworks.com/studio/documents/c72656b5-842d-45d9-b5f6-82b66b09d0b9/edit"
public: "https://hyeonworks.com/questions/server-session-pattern-multi-instance"
---
# 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가
Mediator와 BFF는 브라우저의 로그인 세션과 OAuth token을 서로 다른 저장소에서 관리한다.
현재 구현에서는 두 저장소 모두 애플리케이션 서버의 메모리에 있기 때문에, 서버 프로세스가 종료되면 저장된 상태도 같이 사라진다.
따라서 운영 환경에서 서버를 여러 인스턴스로 구성하게 될 경우 추가 설계가 필요한데, 사용자의 요청이 로그인할 때와 다른 인스턴스로 전달되어도 세션과 토큰을 찾을 수 있어야 하고, 서버가 재시작된 뒤에도 로그인 상태를 유지할 것인지 결정해야 한다.
또한 로그아웃할 때 여러 인스턴스에 걸쳐 저장된 세션과 토큰을 어떻게 같이 제거할지도 정해야 한다.
## 관계
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
두 상태가 모두 process-local memory에 있다는 사실의 출처다.
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
같은 저장소 구성을 쓰는 다른 패턴이다.
- **BFF 인증 구조 설계 기준**
이 질문의 답이 이 기준의 빈 항목을 채운다.
- **BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가**
저장소 후보 비교로 독립시킨 질문이다.
## 사실
- Mediator와 BFF는 로그인 상태와 OAuth Token을 서로 다른 저장소에서 관리한다.
로그인 상태는 HttpSession에 저장하고 session ID로 조회한다.
반면 OAuth Token은 OAuth2AuthorizedClientService에 저장하며, client registration 이름과 principal name을 기준으로 조회한다.
- 현재 세션과 OAuth Token을 저장할 별도의 저장소를 직접 설정하지 않았다.
따라서 Spring Boot의 자동 구성이 고르는 메모리 기반 기본 구현이 사용된다.
다만 코드에 저장소를 직접 생성하는 Bean이 없기 때문에, 어떤 구현체가 실제로 사용되는지는 Spring Boot의 자동 구성 결과까지 확인해야 정확하게 알 수 있다.
- 현재는 Spring Session이나 Redis, JDBC 기반 Token Store와 같은 외부 저장소를 사용하지 않는다.
따라서 로그인 세션과 OAuth Token 정보는 모두 해당 애플리케이션 인스턴스의 메모리에 저장된다.
이 때문에 인스턴스가 종료되거나 재시작되면 해당 인스턴스가 가지고 있던 로그인 세션과 OAuth Token 정보도 같이 없어진다.
- Authorized Client는 세션별로 구분되지 않는다.
조회 기준에 session ID가 없기 때문에 같은 사용자가 여러 브라우저에서 로그인하면 동일한 Authorized Client 정보를 사용하게 된다.
- OAuth2-Proxy 구조에서는 로그인 상태를 별도의 서버 저장소에 보관하지 않고,
필요한 최소한의 정보를 브라우저의 세션 쿠키에 담아 관리한다.
현재 설정에서는 이 쿠키의 유효 시간을 1시간으로 두고 있다.
- 현재 테스트에는 서버가 재시작되거나 사용자의 요청이 다른 인스턴스로 전달된 뒤에도 로그인 상태와 OAuth Token을 정상적으로 사용할 수 있는지 확인하는 항목이 없다.
따라서 세션이나 Token 저장 방식을 변경하더라도 기존 동작이 그대로 유지되는지는 별도로 확인 및 검증이 필요하다.
## 가정
- 운영에서는 인스턴스가 둘 이상이다.
- 재시작과 배포가 로그인 상태를 끊어서는 안 되는데, 지금 구조에서는 끊기게 된다.
- 같은 사용자의 여러 브라우저 session이 서로의 token 항목을 덮어써서는 안 된다.
## 미지수
- 재시작 뒤 로그인이 유지되는가. 지금은 안 된다는 것까지 알지만 무엇을 바꿔야 되는지는 정하지 않았다.
- 인스턴스가 바뀌어도 같은 session을 찾게 되는가.
- 같은 사용자의 여러 session이 authorized client 항목을 공유하거나 덮어쓰게 되는가.
한쪽에서 로그아웃하면 다른 쪽도 끊기게 되는가.
- 저장된 refresh token이 암호화되는가.
저장소를 여는 사람이 그 값을 그대로 읽게 되는가.
- logout에서 HttpSession과 authorized client를 모두 정리하는가.
한쪽만 삭제했을 때 다음 요청이나 재로그인에서 어떤 상태가 복원되는가.
- session 만료와 token 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 어떻게 보이게 되는가.
- OAuth2-Proxy 구조의 replica들이 같은 cookie secret을 어떻게 공유하고 교체하게 되는가.
교체하는 동안 로그인해 있던 사람은 어떻게 되는가.
## 제약
- 현재 구조는 단일 인스턴스로 실행하고 있어 replica 간 session 조회와 failover 동작은 아직 구현되지 않았다.
- Authorized Client는 session ID를 기준으로 저장하거나 조회하지 않는다.
따라서 여러 인스턴스가 같은 세션을 사용할 수 있도록 Session Store를 공유 저장소로 변경하는 것만으로는 충분하지 않다.
로그인 세션을 여러 인스턴스에서 공유하는 방법과 OAuth Token이 저장된 Authorized Client를 어떻게 저장하고 공유할지는 각각 별도로 설계해야 한다.
- Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 network에서 강제된 상태가 아니다.
## 선택지
### 1. 공유 저장소를 사용한다
HttpSession과 Authorized Client를 모두 외부의 공유 저장소에 보관하면 여러 애플리케이션 인스턴스가 동일한 로그인 세션과 OAuth Token 정보를 조회할 수 있다.
따라서 사용자의 요청이 다른 인스턴스로 전달되거나 특정 인스턴스가 재시작되더라도 기존 로그인 상태를 계속 사용할 수 있다.
다만 인증 과정이 외부 저장소에 의존하게 되므로 추가로 고려해야 할 사항이 생긴다.
저장소에 장애가 발생했을 때 인증 요청을 어떻게 처리할지 정해야 하고, 세션과 Token을 어떤 형식으로 저장할지와 저장된 Token을 어떻게 보호할지도 결정해야 한다.
또한 세션은 남아 있는데 Token은 이미 만료되는 것과 같은 불일치가 발생하지 않도록 두 상태의 만료 시간과 제거 시점도 함께 설계해야 한다.
### 2. session affinity로 같은 인스턴스에 붙인다
Sticky Session을 사용하면 같은 세션의 요청을 가능한 한 동일한 애플리케이션 인스턴스로 전달할 수 있다.
기존의 메모리 기반 세션과 Token 저장 방식을 그대로 사용할 수 있기 때문에 애플리케이션 코드의 변경이 적고 별도의 공유 저장소도 필요하지 않다.
하지만 해당 인스턴스가 종료되면 그 인스턴스의 메모리에 저장되어 있던 로그인 세션과 OAuth Token 정보도 함께 사라진다. 따라서 배포나 오토스케일링으로 인스턴스가 자주 교체되는 환경에서는 Sticky Session만으로 로그인 상태를 안정적으로 유지하기 어렵고, 인스턴스가 사라졌을 때 상태를 어떻게 복구할지 별도로 설계해야 한다.
### 3. 브라우저가 token을 들고 API를 직접 부른다
서버에 로그인 세션이나 OAuth Token 상태를 저장하지 않는 구조로 바꾸면, 여러 인스턴스가 공유해야 할 상태 자체가 없어지므로 별도의 공유 저장소나 Session Affinity가 필요하지 않다.
Resource Server는 각 요청에 포함된 Access Token을 검증하여 요청을 처리한다.
SPA처럼 브라우저가 OAuth Token을 직접 보관하고 API 요청에 사용하는 구조가 여기에 해당한다.
다만 브라우저에 OAuth Token을 노출하지 않아야 한다면 이 선택지는 제외한다.
### 4. 층이 다른 선택지 — 최소 정보만 담은 client-side cookie
이 방식은 기존 세션이나 Token 저장소를 다른 저장소로 교체하는 방법이 아니다.
서버에 인증 상태를 저장하는 구조 자체를 없애고, 필요한 인증 상태를 쿠키에 담아 전달하는 방식으로 변경하는 것이다.
따라서 공유 저장소나 Sticky Session처럼 기존 서버 상태를 어떻게 유지할지 결정하는 방법과 같이 비교하긴 어렵다.
Forward-Auth 구조로 전환하면 애플리케이션이 OAuth Token을 서버에 직접 저장하고 관리할 필요가 없어진다.
대신 여러 인스턴스가 동일한 인증 쿠키를 처리할 수 있도록 Cookie Secret을 공유해야 한다.
또한 인증 프록시가 전달하는 사용자 정보를 애플리케이션이 신뢰하게 되므로, 외부 요청이 해당 헤더를 위조할 수 없도록 네트워크 접근 경로와 전달 헤더를 함께 관리해야 한다.
## 다음 검증
인스턴스를 둘로 띄우고 순서대로 확인한다.
1. 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 유지되는지 본다.
2. 한 인스턴스를 재시작하고 같은 session cookie로 로그인 상태가 남는지 본다.
3. 같은 사용자로 두 브라우저에서 로그인해 authorized client 항목이 서로를 덮어쓰는지 본다.
4. 한쪽에서 logout한 뒤 다른 쪽 요청이 어떻게 되는지 본다.
5. session 만료를 token 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다.
여기서 확인한 결과로 선택지를 좁힌다.
@@ -0,0 +1,105 @@
---
id: 9ae4ec71-a32e-49a7-88c2-f7368541c28d
kind: QUESTION
slug: refresh-rotation-replica-contention
title: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
topic: 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회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다.