feat: 문서 구조 변경 및 tech-visual 스킬 추가
This commit is contained in:
+133
@@ -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 교체 절차는 후보를 고른 뒤에 따로 설계한다.
|
||||
+124
@@ -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을 바꾼 뒤 몇 번째 요청부터 반영되는지도 확인한다.
|
||||
+131
@@ -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 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다.
|
||||
|
||||
여기서 확인한 결과로 선택지를 좁힌다.
|
||||
+105
@@ -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회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다.
|
||||
Reference in New Issue
Block a user