refactor: 문서 개선 중
This commit is contained in:
+2
-2
@@ -113,7 +113,7 @@ HTTP : o
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
confidential client는 client secret을 서버에 두고 자기를 인증할 수 있는 애플리케이션이다. 여기 나오는 mediator가 그런 클라이언트이고, 브라우저 대신 authorization code를 토큰으로 바꿔 서버에 보관한다. 다만 보호 자원 서버(Resource Server)는 브라우저가 직접 부른다. 리프레시 토큰만 브라우저에서 걷어내면 브라우저가 무엇을 계속 다루게 되는지 확인했다.
|
||||
AP2의 mediator는 server에서 client credential을 보호하고 client authentication을 수행할 수 있는 confidential client다. 이 프로젝트에서는 `client_secret_basic`을 사용하고 client secret을 서버에 둔다. Mediator가 브라우저 대신 authorization code를 토큰으로 바꿔 서버에 보관하지만, 보호 자원 서버(Resource Server)는 브라우저가 직접 부른다. 리프레시 토큰만 브라우저에서 걷어내면 브라우저가 무엇을 계속 다루게 되는지 확인했다.
|
||||
|
||||
## 서버로 옮긴 값과 브라우저로 돌아오는 값
|
||||
|
||||
@@ -241,7 +241,7 @@ repeatable GET
|
||||
|
||||
## 이 구조를 고를 때 함께 오는 서버 상태와 액세스 토큰 노출
|
||||
|
||||
mediator를 넣은 이유는 하나다. 리프레시 토큰은 브라우저 JavaScript 메모리에서 서버로 옮기고, 브라우저가 보호 자원 서버를 직접 부르는 방식은 바꾸지 않으려고 했다. 둘을 같이 두려면 client secret을 서버에 보관할 수 있는 confidential client가 필요하다. 구현을 마치고 보니 이 선택은 두 비용을 함께 남겼다.
|
||||
mediator를 넣은 이유는 하나다. 리프레시 토큰은 브라우저 JavaScript 메모리에서 서버로 옮기고, 브라우저가 보호 자원 서버를 직접 부르는 방식은 바꾸지 않으려고 했다. 이 프로젝트에서는 mediator가 server-side에서 client credential을 보호하고 `client_secret_basic`으로 자신을 인증하도록 confidential client로 구성했다. 구현을 마치고 보니 이 선택은 두 비용을 함께 남겼다.
|
||||
|
||||
- 서버 상태 : mediator의 HttpSession과 authorized-client 저장소를 운영해야 한다
|
||||
- 브라우저 노출 : 액세스 토큰이 응답 본문과 `Authorization` 헤더를 지나가는 것은 막지 못했다
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ AP1에서는 SPA(Single Page Application)를 public OAuth client로 구성하고
|
||||
- **Authorization Code Flow의 Endpoint와 Credential 이동 기준**
|
||||
SPA에서 authorization request를 보내고 authorization code를 받은 뒤 토큰을 교환하는 과정을 직접 확인한 내용이다.
|
||||
- **Public Client와 Confidential Client 구분 기준**
|
||||
SPA는 client secret을 안전하게 숨길 수 없기 때문에 public client로 구성했고 PKCE S256을 사용했다.
|
||||
SPA는 브라우저 실행 환경에서 장기 client credential의 기밀성을 유지하기 어려워 public client로 구성했고 PKCE S256을 사용했다.
|
||||
- **OAuth Token과 Application Session을 구분하는 기준**
|
||||
JavaScript 메모리에 있는 토큰과 Keycloak의 SSO 쿠키는 서로 다른 상태여서, 새로고침으로 SPA의 토큰이 없어져도 Keycloak의 SSO까지 끝나는 것은 아니다.
|
||||
|
||||
@@ -98,7 +98,7 @@ HTTP : o
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
public client는 브라우저처럼 client secret을 안전하게 숨길 수 없는 애플리케이션이다. AP1의 SPA가 그런 클라이언트라 authorization code를 토큰으로 바꾸는 일까지 브라우저가 직접 하고, 받은 액세스·리프레시·ID 토큰은 `InMemoryWebStorage`를 써서 실행 중 메모리에만 둔다. 이 구성이 무엇을 줄이고 무엇은 줄이지 못하는지 확인했다.
|
||||
AP1의 SPA는 브라우저 실행 환경에서 장기 client credential의 기밀성을 유지하기 어려운 public client다. 이 프로젝트는 SPA에 shared client secret을 두지 않았고, authorization code를 토큰으로 바꾸는 일까지 브라우저가 직접 한다. 받은 액세스·리프레시·ID 토큰은 `InMemoryWebStorage`를 써서 실행 중 메모리에만 둔다. 이 구성이 무엇을 줄이고 무엇은 줄이지 못하는지 확인했다.
|
||||
|
||||
## SPA가 토큰을 다루는 위치
|
||||
|
||||
|
||||
+1
-1
@@ -119,6 +119,6 @@ AP2에서는 mediator가 token을 받고 API는 브라우저가 부른다. AP3
|
||||
|
||||
confidential client는 여기에 클라이언트 인증을 더한다. AP3의 `bff-confidential`은 `client_secret_basic`으로 자기 클라이언트를 인증하면서 PKCE S256도 함께 쓴다. Spring Security에서는 authorization request를 만드는 resolver에 `OAuth2AuthorizationRequestCustomizers.withPkce()`를 장착한다. 그러면 프레임워크가 `state`와 verifier를 만든다.
|
||||
|
||||
클라이언트 설정에 S256을 강제하는 속성이 없고, 테스트도 authorization request의 challenge를 검사하지 않는다. Authorization Code Flow를 쓴다는 것까지는 확인했고, PKCE S256이 고정됐는지는 확인하지 않았다.
|
||||
반면 AP2의 `token-mediating-confidential`은 client 설정에서 S256을 강제하지 않고 AP2 E2E도 authorization request의 challenge를 검사하지 않는다. 따라서 AP2는 Authorization Code를 사용하는 confidential client라는 사실까지 확인했으며, PKCE S256이 고정되었다고 기록하지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+6
-10
@@ -10,9 +10,13 @@ status: 게시 전
|
||||
version: 4
|
||||
basisVersion: Keycloak 26.7.0 · Spring Security OAuth2 Resource Server
|
||||
studio: "https://hyeonworks.com/studio/documents/87000d59-b69f-4010-9481-0b71c8bde32d/edit"
|
||||
assets:
|
||||
- key: bearer-jwt-validation-chain
|
||||
file: ../../../final/assets/bearer-jwt-validation-chain/bearer-jwt-validation-chain.svg
|
||||
sourceRevision: keycloak-patterns-lab@2026-08
|
||||
source:
|
||||
- final/document.md#선택의-이유와-지킨-경계-ap1
|
||||
- final/document.md#선택이-코드와-흐름에-반영되는-방식-ap1-완주
|
||||
---
|
||||
|
||||
# Bearer JWT가 인증된 principal이 되기까지
|
||||
@@ -47,17 +51,9 @@ Spring이 받는 것은 이 헤더에 실려 온 문자열 그대로다. 요청
|
||||
|
||||
## 변환 순서
|
||||
|
||||
직접 만든 코드가 지나는 순서는 다음과 같다.
|
||||
검증과 변환은 다음 순서로 이어진다.
|
||||
|
||||
```text label="raw Bearer JWT가 principal이 되기까지"
|
||||
raw Bearer JWT
|
||||
→ NimbusJwtDecoder(JWK signature)
|
||||
→ default issuer + timestamp validators
|
||||
→ AudienceValidator("keycloak-pattern-api")
|
||||
→ validated Jwt
|
||||
→ KeycloakRealmRoleConverter
|
||||
→ authenticated principal + ROLE_* authorities
|
||||
```
|
||||

|
||||
|
||||
헤더를 꺼내 JWT 인증 제공자에 넘기고 설정된 디코더를 부르는 일은 Spring OAuth2 Resource Server가 한다. 저장소의 코드는 Spring 내부 필터를 직접 만들지 않으므로, 이 순서에는 설정 DSL(Domain Specific Language, 설정 전용 문법)이 붙여 주는 부분과 직접 만들어 끼운 빈이 함께 들어 있다.
|
||||
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ source:
|
||||
| Local Storage | 유지 | 읽는다 | 붙지 않는다 |
|
||||
| HttpOnly 쿠키 | 만료까지 유지 | 읽지 못한다 | 붙는다 |
|
||||
|
||||
네 곳 가운데 쿠키만 요청에 자동으로 붙기 때문에, 쿠키를 자격 증명으로 쓰면 그 요청이 사용자가 의도한 것인지 확인하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 따라붙는다.
|
||||
네 곳 가운데 쿠키만 요청에 자동으로 붙기 때문에, 쿠키를 자격 증명으로 쓰면 공격 사이트가 쿠키만 이용해 만든 cross-site forged request와 애플리케이션이 anti-CSRF token을 가지고 만든 요청을 구분하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 따라붙는다.
|
||||
|
||||
## userStore와 stateStore를 나눈다
|
||||
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ source:
|
||||
|
||||
# Cookie로 인증하는 요청에서 CSRF token이 하는 일
|
||||
|
||||
세션 쿠키는 브라우저가 요청마다 자동으로 붙이기 때문에, 상태를 바꾸는 요청이 사용자가 의도한 것인지 서버가 따로 확인해야 한다. CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 토큰이 그 확인을 맡고, SameSite는 브라우저가 쿠키를 언제 보낼지 정하는 별도의 정책이다.
|
||||
세션 쿠키는 브라우저가 요청마다 자동으로 붙이기 때문에, 상태를 바꾸는 요청에는 쿠키만으로 만든 cross-site forged request를 구분할 별도 검증 값이 필요하다. CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 토큰은 예상한 anti-CSRF token이 함께 온 요청인지 검사하고, SameSite는 브라우저가 쿠키를 언제 보낼지 정하는 별도의 정책이다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -100,7 +100,7 @@ theme=dark
|
||||
|
||||
## SameSite가 정하는 것과 CSRF token이 정하는 것
|
||||
|
||||
SameSite는 쿠키를 다른 사이트로 보낼지 브라우저가 정하는 정책이고, CSRF 토큰은 상태를 바꾸는 요청이 사용자의 의도인지 서버가 검증하는 애플리케이션 규약이다.
|
||||
SameSite는 쿠키를 다른 사이트로 보낼지 브라우저가 정하는 정책이고, CSRF 토큰은 cookie가 자동 첨부되는 상태 변경 요청에 별도 검증 값을 요구해 cross-site forged request를 구분하는 애플리케이션 규약이다.
|
||||
|
||||
| | SameSite | CSRF 토큰 |
|
||||
|---|---|---|
|
||||
|
||||
+5
-9
@@ -10,6 +10,9 @@ status: 게시 전
|
||||
version: 4
|
||||
basisVersion: Keycloak 26.7.0 identity brokering
|
||||
studio: "https://hyeonworks.com/studio/documents/d99fdec9-fe9e-4e0f-a50b-6fb9b9ed5719/edit"
|
||||
assets:
|
||||
- key: idp-broker-upstream-downstream-boundary
|
||||
file: ../../../final/assets/idp-broker-upstream-downstream-boundary/idp-broker-upstream-downstream-boundary.svg
|
||||
sourceRevision: keycloak-patterns-lab@2026-08
|
||||
source:
|
||||
- final/document.md#선택이-코드와-흐름에-반영되는-방식-google-login
|
||||
@@ -36,16 +39,9 @@ source:
|
||||
|
||||
사용자가 브로커 로그인 화면에서 어느 외부 IdP로 로그인할지 고르면 인증 왕복이 두 번 일어난다. 앞의 왕복은 브로커와 외부 IdP 사이에서, 뒤의 왕복은 애플리케이션과 브로커 사이에서 일어난다. 브로커보다 앞에 있는 외부 IdP 쪽을 upstream이라고 부른다.
|
||||
|
||||
먼저 브라우저가 외부 IdP에 authorization 요청을 보내고, 브로커는 돌아온 응답을 검증해 자기 realm의 사용자와 연결한다. 그다음 애플리케이션으로 나가는 값은 브로커가 다시 만든다. 무엇이 무엇으로 바뀌는지 순서로 적으면 이렇다.
|
||||
먼저 브라우저가 외부 IdP에 authorization 요청을 보내고, 브로커는 돌아온 응답을 검증해 자기 realm의 사용자와 연결한다. 그다음 애플리케이션으로 나가는 값은 브로커가 다시 만든다. Upstream IdP와 애플리케이션 경계는 다음처럼 이어진다.
|
||||
|
||||
```text label="brokering 변환 순서"
|
||||
Google identity assertion
|
||||
→ Keycloak broker validation
|
||||
→ provider alias + upstream sub로 account identity 결정
|
||||
→ Keycloak local user/session
|
||||
→ Keycloak authorization code
|
||||
→ AP1·AP2·AP3·AP4 중 선택한 downstream 경계
|
||||
```
|
||||

|
||||
|
||||
외부 IdP가 보낸 것은 첫 줄의 `Google identity assertion` 하나이고, 그 아래 `Keycloak local user/session`과 `Keycloak authorization code`는 브로커가 만든다.
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ source:
|
||||
두 저장 구조를 공유 저장소로 옮길 때는 각각 따로 설계해야 한다.
|
||||
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
|
||||
저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
|
||||
- 여기까지는 한 대에서 실행한 학습 환경에서 코드와 테스트로 확인한 것이다.
|
||||
- 현재 확인 범위는 한 대에서 실행한 학습 환경의 코드와 테스트다.
|
||||
여러 인스턴스가 세션과 Authorized Client를 공유하는지는 실행해 확인하지 못했다.
|
||||
|
||||
## 가정
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ source:
|
||||
|
||||
이러면 엣지는 애플리케이션의 역할과 테넌트, 권한 정책을 알 필요가 없다. BFF가 사용자와 권한을 조회해 인가를 판단하고, 화면에 필요한 여러 Resource Server의 API를 불러 결과를 합칠 수 있다.
|
||||
|
||||
대신 BFF를 넣으면 서버가 다시 인증 상태를 들고 있어야 한다. 브라우저와 BFF 사이의 애플리케이션 세션을 보호해야 한다. 쿠키 기반 세션을 쓴다면 쿠키가 요청마다 자동으로 붙으니, 값을 바꾸는 요청에는 사용자가 의도한 것인지 확인하는 CSRF(Cross-Site Request Forgery) 검증도 있어야 한다. BFF를 여러 대로 늘려 인증 상태를 유지하려면 세션과 authorized client를 어떻게 공유할지 정하고, 공유 저장소의 장애와 만료 처리까지 운영해야 한다.
|
||||
대신 BFF를 넣으면 서버가 다시 인증 상태를 들고 있어야 한다. 브라우저와 BFF 사이의 애플리케이션 세션을 보호해야 한다. 쿠키 기반 세션을 쓴다면 쿠키가 요청마다 자동으로 붙으니, 값을 바꾸는 요청에는 별도 anti-CSRF token을 요구해 cross-site forged request를 구분하는 CSRF(Cross-Site Request Forgery) 검증도 있어야 한다. BFF를 여러 대로 늘려 인증 상태를 유지하려면 세션과 authorized client를 어떻게 공유할지 정하고, 공유 저장소의 장애와 만료 처리까지 운영해야 한다.
|
||||
|
||||
이 구조를 골랐다가 다시 엣지 쪽으로 되돌린다면 BFF가 맡던 사용자별 인가를 업스트림이나 별도 정책 서비스로 다시 옮겨야 한다.
|
||||
|
||||
|
||||
+12
-12
@@ -65,29 +65,29 @@ Mediator와 BFF(Backend for Frontend)는 브라우저의 로그인 세션과 OAu
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. 공유 저장소를 사용한다
|
||||
### 1. 공유 저장소 — 상태 일관성과 저장소 가용성
|
||||
|
||||
HttpSession과 Authorized Client를 모두 바깥의 공유 저장소에 두면 여러 애플리케이션 인스턴스가 같은 로그인 세션과 OAuth 토큰을 조회할 수 있다. 요청이 다른 인스턴스로 가거나 인스턴스 하나가 재시작해도 기존 로그인 상태를 그대로 쓸 수 있다.
|
||||
HttpSession과 Authorized Client를 외부 공유 저장소에 두면 요청이 다른 인스턴스로 이동해도 같은 로그인 상태와 OAuth 토큰을 조회할 수 있다. 인스턴스 재시작과 라우팅 변경에서 상태를 이어 가는 대신 인증 경로가 그 저장소의 가용성에 의존한다.
|
||||
|
||||
저장소에 장애가 나면 인증 요청을 어떻게 처리할지 정해야 하고, 세션과 토큰을 어떤 형식으로 저장할지와 저장한 토큰을 어떻게 보호할지도 정해야 한다. 세션은 아직 유효한데 토큰은 이미 만료된 것 같은 어긋남이 생기지 않도록, 두 상태의 만료 시간과 지우는 시점도 함께 설계해야 한다.
|
||||
이 선택의 핵심은 두 상태의 수명과 보호 방식이다. 세션과 토큰의 만료 시점, 로그아웃 때 지우는 순서, 저장 token 보호가 맞지 않으면 한쪽 상태만 남을 수 있다.
|
||||
|
||||
### 2. session affinity로 같은 인스턴스에 붙인다
|
||||
### 2. session affinity — 라우팅은 고정하지만 node loss는 남는다
|
||||
|
||||
Sticky Session은 같은 세션에서 온 요청을 되도록 같은 애플리케이션 인스턴스로 보내는 방식이다. 지금의 메모리 기반 세션·토큰 저장을 그대로 두어도 되므로 애플리케이션 코드는 거의 손대지 않고, 공유 저장소도 따로 두지 않는다.
|
||||
Sticky Session은 같은 세션의 요청을 되도록 같은 인스턴스로 보낸다. 기존 메모리 저장 구조를 유지할 수 있다는 장점은 있지만 상태를 다른 인스턴스에 복제하지는 않는다.
|
||||
|
||||
다만 그 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 함께 사라진다. 배포나 오토스케일링으로 인스턴스가 자주 바뀌는 환경이라면 Sticky Session만으로 로그인 상태를 지키기 어렵고, 인스턴스가 사라졌을 때 상태를 어떻게 되살릴지 따로 설계해야 한다.
|
||||
고정된 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 사라진다. 따라서 이 선택은 정상 라우팅 중의 이동을 줄이는 방법이지, node loss 뒤 상태 복구 방법은 아니다.
|
||||
|
||||
### 3. 브라우저가 토큰을 들고 API를 직접 부른다
|
||||
### 3. 브라우저 토큰 — 공유할 서버 상태 자체를 없앤다
|
||||
|
||||
서버에 로그인 세션이나 OAuth 토큰을 두지 않는 구조로 바꾸면 여러 인스턴스가 나눠 가질 상태 자체가 없어서, 공유 저장소도 session affinity도 필요하지 않다. Resource Server는 요청마다 실려 온 Access Token을 검증해서 처리한다.
|
||||
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 들고 Resource Server를 직접 호출하면 애플리케이션 인스턴스가 공유할 로그인 세션과 OAuth 토큰 저장소가 사라진다. Resource Server는 요청마다 전달된 Access Token을 검증한다.
|
||||
|
||||
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 직접 들고 API를 부르는 구조가 여기에 해당한다. 다만 브라우저에 OAuth 토큰을 노출하지 않는 것이 조건이라면 이 선택지는 뺀다.
|
||||
대신 자격 증명 소유권이 브라우저로 이동한다. 브라우저에 OAuth 토큰을 노출하지 않는 것이 요구사항이면 저장소 문제를 없애더라도 이 선택은 조건과 충돌한다.
|
||||
|
||||
### 4. 층이 다른 선택지 — 최소 정보만 담은 client-side cookie
|
||||
### 4. client-side cookie — 저장소 문제가 아니라 신뢰 경계가 바뀐다
|
||||
|
||||
이 방식은 세션이나 토큰 저장소를 다른 저장소로 바꾸는 것이 아니다. 서버에 인증 상태를 두는 구조 자체를 없애고, 필요한 인증 상태만 쿠키에 담아 보낸다. 그래서 공유 저장소나 Sticky Session처럼 서버 상태를 어떻게 지킬지 정하는 방법과 나란히 놓고 견주기 어렵다.
|
||||
Forward-Auth의 최소 client-side cookie는 공유 저장소나 Sticky Session과 같은 층의 선택지가 아니다. 서버에 application-owned OAuth 상태를 두는 구조를 줄이고 인증 프록시가 cookie를 검증한 결과를 upstream identity로 전달한다.
|
||||
|
||||
Forward-Auth는 실제 요청을 넘기기 전에 별도의 인증 엔드포인트에 허용 여부를 먼저 묻는 방식이다. 이 구조로 옮기면 애플리케이션이 OAuth 토큰을 서버에 직접 저장하고 관리하지 않아도 된다. 대신 여러 인스턴스가 같은 인증 쿠키를 풀 수 있도록 Cookie Secret을 나눠 가져야 한다. 그리고 인증 프록시가 넘겨 주는 사용자 정보를 애플리케이션이 믿으므로, 바깥 요청이 그 헤더를 위조하지 못하게 네트워크 접근 경로와 전달 헤더를 함께 관리해야 한다.
|
||||
레플리카는 같은 Cookie Secret을 검증할 수 있어야 하고, upstream은 프록시가 전달한 사용자 정보를 신뢰한다. 그래서 이 구조의 핵심 문제는 세션 저장소 일관성보다 secret 배포·교체, 직접 접근 차단, client-supplied identity header 제거 또는 덮어쓰기다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
|
||||
+9
-10
@@ -19,7 +19,7 @@ source:
|
||||
|
||||
# Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
|
||||
|
||||
realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱신하면 이전 refresh token이 바로
|
||||
realm은 refresh token rotation과 최대 재사용 횟수 0회를 쓴다. `refreshTokenMaxReuse`는 시간 창이 아니라 동일 refresh token의 허용 재사용 횟수를 나타낸다. 한 번 갱신하면 이전 refresh token이 바로
|
||||
무효가 되기 때문에, 두 replica가 같은 refresh token으로 동시에 갱신하면 두 번째 사용이 거부될 수 있다.
|
||||
실제 Keycloak 응답과 session에 미치는 영향은 아직 재현하지 않았다.
|
||||
|
||||
@@ -28,7 +28,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
|
||||
- **BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가**
|
||||
이 질문에 답하기 전에 저장소부터 정해야 한다.
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
rotation과 재사용 0회를 쓰는 구성이 여기서 나왔다.
|
||||
rotation과 최대 재사용 횟수 0회를 쓰는 구성이 여기서 나왔다.
|
||||
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
|
||||
인스턴스를 여러 대 띄워야 이 경쟁이 생긴다.
|
||||
- **BFF 인증 구조 설계 기준**
|
||||
@@ -36,12 +36,13 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
|
||||
|
||||
## 사실
|
||||
|
||||
- realm에는 refresh token rotation과 재사용 허용 0회가 설정되어 있다.
|
||||
- realm에는 refresh token rotation과 최대 재사용 횟수 0회가 설정되어 있다.
|
||||
- 커밋된 테스트는 새 refresh token이 발급되는지, 이전 token이 거부되는지, revocation 뒤 refresh가
|
||||
실패하는지를 확인한다. 다만 이 셋은 refresh token을 직접 써서 받은 결과라, 애플리케이션이 스스로
|
||||
갱신할 때도 같은 결과가 나온다고 보지 않는다.
|
||||
- authorized client manager에는 refresh-token provider가 구성되어 있어, access token이 만료되면
|
||||
refresh를 시도할 수 있다.
|
||||
- `refreshTokenMaxReuse`는 재사용 횟수 설정이고 refresh token lifespan은 별도의 시간 설정이다. 이 문서에서는 둘을 같은 허용 시간으로 취급하지 않는다.
|
||||
- access token이 만료될 때까지 기다려 실제로 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
|
||||
- 현재 authorized client 저장소는 프로세스 안에만 있어서(process-local) replica끼리 같은 refresh token
|
||||
상태를 공유하지 않는다. 그래서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
|
||||
@@ -56,7 +57,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
|
||||
## 미지수
|
||||
|
||||
- 같은 refresh token으로 두 replica가 동시에 갱신하면 각 replica가 어떻게 동작하는가.
|
||||
- 재사용 허용 0회에서 두 번째 사용이 거부되면 사용자 화면에 무엇이 보이는가.
|
||||
- 최대 재사용 횟수 0회에서 두 번째 사용이 거부되면 사용자 화면에 무엇이 보이는가.
|
||||
로그인 만료로 보이는가, 일시적 오류로 보이는가.
|
||||
- 저장소에서 새 token을 다시 읽어 재시도하면 성공하는가, 아니면 재인증까지 해야 하는가.
|
||||
- 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
|
||||
@@ -65,7 +66,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
|
||||
|
||||
## 제약
|
||||
|
||||
- rotation과 재사용 0회는 이미 realm에 설정했다. 이 전제는 바꾸지 않고 답한다.
|
||||
- rotation과 최대 재사용 횟수 0회는 이미 realm에 설정했다. 이 전제는 바꾸지 않고 답한다.
|
||||
- 이미 발급된 access token은 만료 전까지 쓸 수 있으므로 refresh 실패가 곧바로 드러나지 않을 수 있다.
|
||||
재현 테스트는 access token 만료 직후에 맞춰 실행한다.
|
||||
- 이 경쟁은 저장소를 공유한 뒤에야 재현되므로 저장소를 정한 다음에 이어서 푼다.
|
||||
@@ -96,11 +97,9 @@ refresh를 전담하는 구성요소 하나만 refresh token을 쓰고, 다른 r
|
||||
이 구성요소가 멈추면 access token이 만료된 뒤에 갱신할 주체가 없어진다. 그래서 이 구성요소를 어떻게
|
||||
살려 두고 어떻게 복구할지를 먼저 정해야 한다.
|
||||
|
||||
### 4. 제약상 제외 — 재사용 허용을 늘린다
|
||||
### 4. 제약상 제외 — 최대 재사용 횟수를 늘린다
|
||||
|
||||
재사용을 짧게 허용하면 두 번째 사용이 거부되지 않고 코드도 고칠 필요가 없다.
|
||||
다만 rotation과 재사용 0회는 이미 realm에 설정했고, 허용한 시간 안에는 훔친 refresh token이 들어와도
|
||||
막지 못한다.
|
||||
최대 재사용 횟수를 늘리면 동일 refresh token의 추가 사용을 일정 횟수 받아들이도록 구성할 수 있다. 그만큼 탈취된 refresh token의 replay를 허용할 여지도 커진다. 현재 실험은 rotation과 최대 재사용 횟수 0회를 전제로 하므로 이 선택지는 제외한다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
@@ -113,4 +112,4 @@ refresh를 전담하는 구성요소 하나만 refresh token을 쓰고, 다른 r
|
||||
5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다.
|
||||
|
||||
실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
|
||||
rotation과 재사용 0회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다.
|
||||
rotation과 최대 재사용 횟수 0회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다. 정확한 Keycloak runtime의 동시 refresh 결과는 source repository와 실행 환경을 다시 사용할 수 있을 때 realm 값과 함께 별도 evidence로 확인한다.
|
||||
|
||||
+7
-7
@@ -28,7 +28,7 @@ Authorization Endpoint에서 리다이렉트, Token Endpoint, JWK(JSON Web Key)
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
confidential client가 Token Endpoint를 부를 때 자기 자신을 인증한다.
|
||||
- **Public Client와 Confidential Client 구분 기준**
|
||||
클라이언트 종류가 정해져야 PKCE와 클라이언트 인증을 어디에 걸지 정해진다.
|
||||
현재 네 패턴에서 client type, callback/code-exchange ownership, PKCE와 client authentication이 어떻게 배치됐는지 함께 본다.
|
||||
|
||||
## 목적
|
||||
|
||||
@@ -36,24 +36,24 @@ Authorization Endpoint와 Token Endpoint는 하는 일도 다르고 요청이
|
||||
|
||||
하나는 브라우저가 페이지째 넘어가는 full-page navigation이고, 다른 하나는 서버가 보낼 수도 있고 브라우저가 직접 보낼 수도 있는 호출이다.
|
||||
|
||||
Authorization Endpoint 요청은 브라우저 주소창을 지나기 때문에 URL이 히스토리와 서버 로그, referrer에 남고, 이 요청에서는 클라이언트를 인증하지 않는다.
|
||||
Token Endpoint 요청은 값을 요청 본문과 Authorization 헤더에 싣고, 보내는 쪽은 클라이언트 종류에 따라 서버이거나 브라우저이며, 클라이언트 인증을 여기서 한다.
|
||||
Authorization Endpoint 요청 URL은 브라우저 주소창과 히스토리, Authorization Server의 접근 로그에 남을 수 있다. 다른 문서나 origin으로 이동할 때 referrer에 어느 범위까지 전달되는지는 브라우저의 Referrer-Policy와 이동 대상의 관계에 따라 달라진다. 이 요청에서는 client authentication을 수행하지 않는다.
|
||||
Token Endpoint 요청은 값을 요청 본문과 Authorization 헤더에 싣는다. 이 프로젝트에서는 AP1 public SPA가 브라우저에서 이 endpoint를 호출하고 AP2~AP4의 server-side component가 code를 교환한다. 이 호출 위치는 public/confidential client type 자체가 강제하는 것이 아니라 callback과 code exchange를 어느 구성요소가 소유하는지, 그리고 배포 구조를 어떻게 잡았는지에 따라 정해진다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. Authorization Endpoint에는 client_secret을 보내지 않는다
|
||||
|
||||
이 요청은 브라우저 주소창을 통해 나가기 때문에 URL이 주소창과 브라우저 히스토리, 서버 접근 로그에 남고, 링크를 타고 온 경우에는 referrer에도 남는다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. 클라이언트 시크릿이 필요한 인증은 아직 하지 않는다.
|
||||
이 요청은 브라우저 주소창을 통해 나가기 때문에 URL이 주소창과 브라우저 히스토리, Authorization Server 접근 로그에 남을 수 있다. 이후 다른 문서로 이동할 때 referrer에 query까지 전달되는지는 Referrer-Policy와 same-origin/cross-origin 조건에 따라 달라진다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. client authentication용 credential은 이 요청에 싣지 않는다.
|
||||
|
||||
이 목록에 없는 값을 여기에 실으면 그 값도 같은 곳에 함께 남는다.
|
||||
|
||||
### 2. Token Endpoint에서 비로소 클라이언트를 인증한다
|
||||
### 2. confidential client는 Token Endpoint에서 자신을 인증한다
|
||||
|
||||
code를 액세스 토큰으로 바꾸는 요청은 자격 증명을 URL 쿼리 문자열이 아니라 요청 본문과 Authorization 헤더에 싣기 때문에, 클라이언트 인증도 이 요청에서 한다. confidential client는 client_secret_basic처럼 클라이언트 시크릿을 함께 보낸다.
|
||||
code를 액세스 토큰으로 바꾸는 요청은 자격 증명을 URL 쿼리 문자열이 아니라 요청 본문과 Authorization 헤더에 싣기 때문에, client authentication도 이 요청에서 수행할 수 있다. 이 프로젝트의 confidential client들은 `client_secret_basic`을 사용하므로 server-side component가 client secret으로 자신을 인증한다.
|
||||
|
||||
주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 디버그 로그, 리버스 프록시 로그, 추적 도구와 APM(성능 모니터링 도구), 패킷 캡처에 남을 수 있어서 자격 증명을 가리는 마스킹을 따로 둔다.
|
||||
|
||||
이 요청을 누가 보내는지는 클라이언트 종류에 따라 갈린다. 서버가 보내면 서버끼리 주고받는 호출이고, 클라이언트 시크릿이 없는 SPA가 보내면 브라우저가 직접 보낸다. Token Endpoint를 서버 안에서만 부르게 하려면 클라이언트 종류부터 confidential client로 정해야 한다.
|
||||
이 프로젝트에서는 AP1 public SPA가 브라우저에서 token endpoint를 호출하고, AP2~AP4의 confidential component가 server-side에서 code를 교환한다. 다만 public/confidential client 구분 자체가 token endpoint의 호출 위치를 강제하는 것은 아니다. 서버 전용 교환은 callback과 code exchange를 server component가 소유하고 client credential이 browser로 노출되지 않도록 설계함으로써 만든다.
|
||||
|
||||
### 3. PKCE는 두 요청을 같은 주체에 묶는다
|
||||
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ BFF(Backend For Frontend)는 화면에 필요한 API를 브라우저 대신 호
|
||||
|
||||
BFF 구조에서는 BFF가 authorization code를 토큰으로 교환하고 그 액세스 토큰으로 Resource Server를 호출한다. 브라우저의 로그인 상태는 세션이 담고 OAuth 토큰은 인가된 클라이언트(authorized client)가 보관하므로, 이 둘을 함께 관리해야 한다. 브라우저 응답에서 토큰이 보이지 않는다고 토큰을 다루는 일까지 없어지지는 않는다. BFF는 요청을 넘겨 주는 프록시가 아니라 로그인 상태와 토큰을 가진 보안 구성요소가 된다.
|
||||
|
||||
쿠키가 자격 증명이 되면 브라우저가 요청마다 자동으로 붙여 보내기 때문에, 값을 바꾸는 요청은 사용자가 의도한 것인지 따로 확인해야 한다. 서버를 재시작하거나 요청이 다른 레플리카로 가더라도 로그인 상태를 이어 갈 저장소도 함께 필요하다.
|
||||
쿠키가 자격 증명이 되면 브라우저가 요청마다 자동으로 붙여 보내기 때문에, 값을 바꾸는 요청에는 cookie만으로 만든 cross-site forged request를 구분할 CSRF 검증이 필요하다. 서버를 재시작하거나 요청이 다른 레플리카로 가더라도 로그인 상태를 이어 갈 저장소도 함께 필요하다.
|
||||
|
||||
쿠키로 인증하는 요청을 지킬 CSRF 검증, OAuth 토큰을 보관할 인가된 클라이언트 저장소, 로그아웃할 때 세션과 토큰을 함께 지우는 방법을 같이 설계해야 한다. BFF가 호출한 Resource Server에서 오류가 났을 때 이를 브라우저에 무엇으로 바꿔 돌려줄지도 함께 정해야 한다.
|
||||
|
||||
@@ -51,7 +51,7 @@ BFF는 이 세션을 확인한 뒤, 보관해 둔 액세스 토큰으로 Authori
|
||||
|
||||
### 2. 쿠키가 자격 증명이면 상태 변경 요청에 CSRF 검증을 둔다
|
||||
|
||||
BFF 구조에서는 브라우저가 요청할 때 세션 쿠키를 자동으로 보내기 때문에, 데이터를 만들고 고치고 지우는 것처럼 서버의 상태를 바꾸는 요청에는 그 요청이 실제 사용자의 의도에서 나왔는지 확인하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 필요하다.
|
||||
BFF 구조에서는 브라우저가 요청할 때 세션 쿠키를 자동으로 보내기 때문에, 데이터를 만들고 고치고 지우는 것처럼 서버의 상태를 바꾸는 요청에는 예상한 anti-CSRF token이 함께 왔는지 검사해 cross-site forged request를 구분하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 필요하다.
|
||||
|
||||
현재 구성에서는 서버가 CSRF 토큰을 쿠키로 내려보내고, JavaScript가 그 값을 읽어 요청 헤더에 다시 담아 보낸다. 서버는 쿠키와 헤더를 함께 확인해 요청을 검증한다.
|
||||
|
||||
|
||||
+7
-7
@@ -31,9 +31,9 @@ Google 로그인을 붙였다고 해서 SPA, Mediator, BFF, OAuth2-Proxy에 이
|
||||
|
||||
## 목적
|
||||
|
||||
외부 IdP(Identity Provider)는 Keycloak 앞에 서서 사용자 인증을 실제로 수행하는 인증 공급자이고, Google이 그중 하나다. 사용자가 Keycloak 로그인 화면에서 Google을 고르면 브라우저는 Google로 이동해 인증을 마치고, Keycloak이 그 결과를 확인해 자신이 가진 사용자 정보와 연결한다. 그런 다음 Keycloak이 애플리케이션에 자신이 발급한 Authorization Code를 전달한다.
|
||||
외부 IdP(Identity Provider)는 Keycloak 앞에서 사용자 인증을 수행한다. Google이 그중 하나다. 사용자가 Keycloak 로그인 화면에서 Google을 고르면 브라우저는 Google에서 인증을 마친다. Keycloak은 그 결과를 검증해 realm 사용자와 연결한 뒤 자신이 발급한 Authorization Code를 애플리케이션에 전달한다.
|
||||
|
||||
그래서 Google 같은 외부 IdP를 추가해도 애플리케이션의 인증 구조는 달라지지 않는다. 토큰을 브라우저가 직접 받을지 서버에서 관리할지, 브라우저와 서버 중 어느 계층이 Resource Server의 API를 호출할지는 기존 SPA, Mediator, BFF, OAuth2-Proxy 구조 중 무엇을 골랐는지가 정한다.
|
||||
Google 같은 외부 IdP가 추가돼도 애플리케이션이 상대하는 OAuth 경계는 Keycloak이다. 토큰을 브라우저가 받을지 서버가 관리할지와 Resource Server를 누가 호출할지는 기존 SPA, Mediator, BFF, OAuth2-Proxy 구조가 정한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
@@ -43,7 +43,7 @@ Google 로그인을 붙였다고 해서 SPA, Mediator, BFF, OAuth2-Proxy에 이
|
||||
|
||||
Google에서 인증이 끝나면 그 결과는 먼저 Keycloak이 검증한다. 이후 애플리케이션이 쓰는 Authorization Code와 토큰은 Google이 아니라 Keycloak이 발급한 것이고, Resource Server가 검증하는 토큰도 Keycloak이 발급한 것이다.
|
||||
|
||||
로그인 화면에서 Google이나 다른 IdP를 고르게 하거나, IdP마다 다른 계정을 Keycloak 사용자와 어떻게 연결할지를 따로 처리하는 것은 자연스럽다. 다만 Resource Server가 Google이 발급한 토큰과 Keycloak이 발급한 토큰을 각각 다르게 검증하거나, 애플리케이션의 인가 로직이 로그인에 쓴 IdP에 따라 갈리기 시작한다면 외부 IdP와 애플리케이션을 갈라놓던 Keycloak의 역할이 제대로 지켜지고 있는지 확인할 필요가 있다.
|
||||
로그인 화면에서 IdP를 고르는 일과 외부 계정을 Keycloak 사용자에 연결하는 일은 broker 경계 안에 있다. Resource Server가 Google token과 Keycloak token을 따로 검증하거나 애플리케이션 인가가 로그인 IdP에 따라 갈리기 시작하면 이 경계가 애플리케이션까지 확장된 것이다.
|
||||
|
||||
### 2. 외부 계정은 IdP와 subject 조합으로 식별한다
|
||||
|
||||
@@ -55,15 +55,15 @@ Google에서 인증이 끝나면 그 결과는 먼저 Keycloak이 검증한다.
|
||||
|
||||
### 3. 이메일 충돌은 별도의 계정 연결 문제로 다룬다
|
||||
|
||||
외부 IdP가 전달한 이메일 주소가 기존 계정의 이메일과 같더라도, 이메일이 같다는 사실만으로 두 계정이 같은 사용자의 것이라고 확신할 수 없기 때문에 자동으로 연결하지 않는다.
|
||||
외부 IdP가 전달한 이메일이 기존 계정과 같아도 자동으로 연결하지 않는다. 같은 이메일이라는 사실만으로 두 계정의 소유자가 같다고 확인할 수 없기 때문이다.
|
||||
|
||||
계정을 연결해야 한다면 기존 계정으로 다시 로그인하게 하거나 추가 인증을 요구하는 등, 사용자가 그 계정의 실제 소유자인지 확인하는 절차를 따로 거친다.
|
||||
계정 연결에는 기존 계정 재로그인이나 추가 인증처럼 기존 계정의 소유권을 확인하는 절차가 별도로 필요하다.
|
||||
|
||||
### 4. mock provider 테스트와 실제 IdP 검증을 구분한다
|
||||
|
||||
지금까지 mock provider로 확인한 것은 Keycloak이 외부 IdP의 인증 결과를 정상적으로 받아들이는지와, 필요한 사용자 정보가 올바르게 매핑되는지까지다.
|
||||
현재 확인 범위는 mock provider를 이용한 broker 동작과 claim mapping 계약까지다.
|
||||
|
||||
mock provider 테스트만으로는 실제 외부 IdP와의 연동까지 검증할 수 없다. 실제 계정으로 로그인하는 과정과 공개 HTTPS 콜백, 사용자 동의(Consent) 화면, 외부 IdP가 적용하는 도메인 정책 등은 아직 확인하지 않았다. 그래서 실제 외부 IdP를 연결해 전체 로그인 흐름을 따로 검증해야 한다.
|
||||
실제 계정 로그인, 공개 HTTPS callback, 사용자 동의(Consent), 외부 IdP의 도메인 정책은 확인하지 않았다. 따라서 mock provider 검증 결과를 실제 IdP 운영 연동의 완료 조건으로 쓰지 않는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
|
||||
+9
-9
@@ -41,7 +41,7 @@ SPA(Single Page Application), Mediator, BFF(Backend for Frontend), Forward-Auth
|
||||
|
||||
Forward-Auth 구조에서는 애플리케이션이 OAuth 토큰을 직접 관리하는 책임을 더 줄일 수 있다. 대신 애플리케이션이 엣지에서 넘어온 사용자 정보 헤더를 신뢰하게 되므로, 헤더 위조 방지와 직접 접근 차단, 신뢰할 수 있는 네트워크 경계를 구성해야 한다.
|
||||
|
||||
어느 구조가 더 안전한지를 먼저 정하지 않고, 요구사항마다 무엇을 확인해야 하는지를 본다.
|
||||
선택 기준은 어느 구조가 더 안전한지에 대한 단일 순위가 아니라, 요구사항마다 달라지는 자격 증명 위치와 운영 책임이다.
|
||||
|
||||
## 규칙
|
||||
|
||||
@@ -49,7 +49,7 @@ Forward-Auth 구조에서는 애플리케이션이 OAuth 토큰을 직접 관리
|
||||
|
||||
브라우저가 액세스 토큰을 쓰는지, Resource Server를 누가 호출하는지, 서버가 어떤 인증 상태를 관리하는지, Resource Server가 어떤 자격 증명을 검증하는지, CSRF를 어디에서 처리하는지를 확인한다.
|
||||
|
||||
이 다섯 항목은 패턴 이름 대신 요청 하나를 끝까지 따라가서 채운다. 실제 엔드포인트와 메서드, 중간에 생기는 데이터, 성공 응답과 실패 응답까지 봐야 한다. 같은 질문을 로그인할 때와 로그인 뒤 API를 부를 때 각각 던진다.
|
||||
비교 단위는 패턴 이름이 아니라 요청 한 번의 실제 경로다. 비교 입력에는 엔드포인트와 메서드, 중간에 생기는 자격 증명, 성공·실패 응답이 포함되고 로그인 구간과 로그인 뒤 API 호출 구간을 각각 나눠 본다.
|
||||
|
||||
SPA와 Mediator에서는 브라우저가 액세스 토큰으로 Resource Server를 직접 호출한다. SPA는 Bearer 액세스 토큰을 Authorization 헤더에 직접 넣고 인증에는 쿠키를 쓰지 않는다. Mediator는 로그인 세션과 OAuth 토큰을 서버에서도 관리하고, 로그인이 끝나면 브라우저에 액세스 토큰을 전달한다.
|
||||
|
||||
@@ -65,23 +65,23 @@ Forward-Auth에서는 인증 프록시가 세션을 관리하고, 인증이 끝
|
||||
|
||||
### 3. 선택 조건과 운영 책임을 같이 문서화한다
|
||||
|
||||
어떤 인증 구조를 골랐는지만 적지 않는다. 어떤 보안 요구사항과 운영 조건 때문에 그 구조를 골랐는지 함께 적는다.
|
||||
선택 기록에는 구조 이름과 함께 그 선택을 만든 보안 요구사항과 운영 조건이 들어간다.
|
||||
|
||||
그 구조를 적용하기 어려운 조건도 같이 적는다. 브라우저에 OAuth 토큰을 둘 수 없는 환경에서는 SPA를 고르기 어렵다. 애플리케이션으로 바로 들어오는 경로나 사용자 정보 헤더를 안전하게 통제할 수 없는 환경에서는 Forward-Auth를 적용하기 어렵다.
|
||||
적용이 어려운 조건도 선택 기준의 일부다. 브라우저에 OAuth 토큰을 둘 수 없는 환경에서는 SPA를 고르기 어렵고, 애플리케이션 직접 경로나 사용자 정보 헤더를 통제할 수 없는 환경에서는 Forward-Auth를 적용하기 어렵다.
|
||||
|
||||
### 4. 이름으로 운영 속성을 추정하지 않는다
|
||||
|
||||
실제 운영에 적용할 때는 서버가 재시작되거나 특정 인스턴스에 장애가 나도 로그인 상태를 유지할 수 있는지, 여러 레플리카가 필요한 세션과 토큰 정보를 공유할 수 있는지, 저장소 장애가 났을 때 어떻게 복구할지를 따로 확인해야 한다.
|
||||
운영 조건에는 서버 재시작이나 인스턴스 장애 뒤 로그인 유지 여부, 여러 레플리카의 세션·토큰 상태 공유 방식, 저장소 장애 복구 방식이 포함된다.
|
||||
|
||||
내부 자격 증명이나 암호화 키와 같은 비밀값을 안전하게 보관하고 교체할 수 있는지도 함께 검증한다. 구조를 고를 때 이런 운영 항목까지 같이 적는다.
|
||||
내부 자격 증명과 암호화 키 같은 비밀값의 보관·교체 방식도 같은 운영 조건에 속한다.
|
||||
|
||||
### 5. 자격 증명의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다
|
||||
|
||||
패턴을 바꿀 때는 기존 책임이 어느 계층으로 옮겨 가는지까지 확인해야 한다.
|
||||
패턴이 바뀌면 저장·전달·검증 책임도 다른 계층으로 이동한다.
|
||||
|
||||
예를 들어 Forward-Auth 구조에서는 엣지가 인증된 사용자 정보를 헤더로 애플리케이션에 전달할 수 있다. 처음에는 사용자 이름이나 이메일처럼 인증에 필요한 정보만 전달하더라도, 애플리케이션의 요구사항이 늘면서 역할이나 권한, 도메인에 묶인 사용자 정보까지 헤더에 계속 붙을 수 있다.
|
||||
|
||||
엣지가 전달해야 하는 정보가 이렇게 늘어나고, 인가 판단이나 화면에 필요한 여러 API 응답의 조합까지 애플리케이션에 필요해진다면, 그 책임을 엣지에 계속 얹기보다 BFF에서 인가와 API 호출을 처리하는 구조가 더 맞는지 다시 검토한다.
|
||||
엣지가 전달할 정보가 역할·권한·도메인 정보까지 늘어나고 여러 API 응답의 조합과 인가 판단도 필요해지면, BFF가 인가와 API 호출을 소유하는 구성이 비교 대상이 된다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
@@ -91,7 +91,7 @@ Forward-Auth에서는 인증 프록시가 세션을 관리하고, 인증이 끝
|
||||
|
||||
## 예외
|
||||
|
||||
- 브라우저에 토큰을 둘 수 없고 서버가 API를 조합해야 하면 남는 선택지는 하나다.
|
||||
- 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면, 브라우저에 OAuth token을 둘 수 없고 server-side API composition이 필요할 때 AP3 BFF가 해당 조건을 만족한다.
|
||||
- 학습이나 시연이 목적이면 운영 속성까지 비교하지 않아도 된다.
|
||||
|
||||
## 예시
|
||||
|
||||
+12
-23
@@ -20,9 +20,8 @@ source:
|
||||
|
||||
# Public Client와 Confidential Client 구분 기준
|
||||
|
||||
OAuth 클라이언트의 종류는 클라이언트 시크릿을 안전하게 보관할 수 있는지로 정한다.
|
||||
SPA(Single Page Application)는 브라우저에서 실행되기 때문에 시크릿을 사용자에게 노출하지 않고 보관할 방법이 없다.
|
||||
그래서 SPA는 대개 public client로 등록한다.
|
||||
OAuth 클라이언트 종류는 인증 서버에 대해 장기 자격 증명의 기밀성을 유지하고 신뢰할 수 있는 클라이언트 인증을 수행할 수 있는지로 구분한다.
|
||||
AP1의 SPA(Single Page Application)는 브라우저 실행 환경에서 이 조건을 충족하기 어려워 public client로 두었다. AP2~AP4는 서버 구성요소가 자격 증명을 보호할 수 있어 confidential client로 구성했고, 이 프로젝트에서는 `client_secret_basic`을 사용한다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -31,29 +30,23 @@ SPA(Single Page Application)는 브라우저에서 실행되기 때문에 시크
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
confidential client로 등록해도 액세스 토큰을 브라우저까지 보낼지는 따로 정한다.
|
||||
- **Authorization Code Flow의 Endpoint와 Credential 이동 기준**
|
||||
클라이언트 종류에 따라 token endpoint에서 하는 클라이언트 인증이 달라진다.
|
||||
public client와 confidential client는 토큰 엔드포인트에서 사용하는 클라이언트 인증 방식이 다르다.
|
||||
|
||||
## 목적
|
||||
|
||||
OAuth 클라이언트를 등록하려면 이 클라이언트를 public client로 볼지 confidential client로 볼지부터 정해야 한다.
|
||||
그래야 authorization code를 토큰으로 교환할 때 PKCE(Proof Key for Code Exchange)를 쓸지, 클라이언트 시크릿으로 클라이언트를 인증할지 정할 수 있다.
|
||||
클라이언트 종류가 정하는 범위는 자격 증명 보호와 클라이언트 인증이다. PKCE(Proof Key for Code Exchange)는 authorization code를 교환하는 쪽이 원래 verifier를 가진 주체인지 확인하는 별도 보호 장치라서 클라이언트 인증과 함께 사용할 수 있다.
|
||||
|
||||
클라이언트 종류를 나누는 기준은 클라이언트 시크릿을 사용자에게 노출하지 않고 안전하게 보관할 수 있는지다.
|
||||
SPA는 브라우저에서 실행되기 때문에 코드에 시크릿을 넣어도 개발자 도구 같은 것으로 사용자가 확인할 수 있고, 그래서 시크릿을 안전하게 보관할 수 없는 public client로 구성한다.
|
||||
AP1 SPA는 public client이고 브라우저가 code를 직접 교환한다. AP2~AP4는 서버 구성요소가 confidential client로 code를 교환하며 이 프로젝트에서는 `client_secret_basic`을 사용한다.
|
||||
|
||||
서버나 BFF(Backend for Frontend)는 시크릿을 서버 안에만 두고 브라우저에 전달하지 않을 수 있으므로 confidential client로 구성할 수 있다.
|
||||
|
||||
클라이언트 종류가 토큰을 누가 보관하는지까지 정해 주지는 않는다.
|
||||
클라이언트 종류는 시크릿을 안전하게 보관할 수 있는지로 정하고, 토큰을 브라우저와 서버 중 어디에서 관리할지는 애플리케이션의 인증 구조에 따라 따로 정한다.
|
||||
토큰 보관 위치와 API 호출 주체는 클라이언트 종류와 다른 축이다. 같은 confidential client라도 AP2는 액세스 토큰을 브라우저에 돌려주고, AP3 BFF(Backend for Frontend)와 AP4 프록시는 서버 쪽에서 다음 요청을 만든다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 클라이언트 시크릿을 숨길 수 있는지로 종류를 정한다
|
||||
### 1. 실행 환경에서 장기 자격 증명을 보호할 수 있는지 구분한다
|
||||
|
||||
애플리케이션의 배포 파일이나 실행 중인 메모리에서 사용자가 클라이언트 시크릿을 확인할 수 있다면 그 시크릿은 안전하게 보관할 수 없으므로 public client로 본다.
|
||||
시크릿을 서버 안에만 보관하고 사용자에게 전달되지 않도록 통제할 수 있다면 confidential client로 구성할 수 있다.
|
||||
브라우저 SPA나 사용자 기기에 설치되는 네이티브 앱처럼 배포물을 사용자가 직접 가진 환경에서는 애플리케이션 안에 넣은 장기 자격 증명을 비밀로 유지하기 어렵다. 이런 클라이언트는 public client로 다룬다.
|
||||
|
||||
네이티브 앱은 브라우저에서 실행되지는 않지만 애플리케이션이 사용자 기기에 설치되기 때문에, 배포 파일을 분석하면 안에 들어 있는 클라이언트 시크릿을 확인할 수 있다. 그래서 네이티브 앱도 대개 public client로 다룬다.
|
||||
서버 구성요소는 자격 증명을 사용자에게 배포하지 않고 인증 서버에 자신을 인증할 수 있다. 이 프로젝트는 공유 시크릿과 `client_secret_basic`을 사용한다. Confidential client의 인증 수단은 공유 시크릿 하나로 제한되지 않으며 private key나 mTLS 같은 방식도 가능하다.
|
||||
|
||||
### 2. public client에서 Authorization Code Flow에 PKCE를 함께 쓴다
|
||||
|
||||
@@ -68,7 +61,7 @@ Authorization Server는 처음 받은 code_challenge와 맞춰 보고 같은 요
|
||||
### 3. confidential client에도 PKCE를 함께 쓸 수 있다
|
||||
|
||||
클라이언트 인증과 PKCE는 보호하는 대상이 다르기 때문에 confidential client에서 둘을 같이 쓸 수 있다.
|
||||
클라이언트 인증은 token endpoint에 요청을 보낸 쪽이 그 클라이언트가 맞는지 확인하고, PKCE는 authorization code를 받은 쪽이 로그인을 시작할 때 만든 code_verifier를 가지고 있는지 확인한다.
|
||||
클라이언트 인증은 토큰 엔드포인트에 요청을 보낸 쪽이 등록된 클라이언트인지 확인한다. PKCE는 authorization code를 받은 쪽이 로그인을 시작할 때 만든 `code_verifier`를 가지고 있는지 확인한다.
|
||||
|
||||
### 4. public client에서는 implicit flow와 direct access grant를 끈다
|
||||
|
||||
@@ -81,13 +74,9 @@ direct access grant는 애플리케이션이 사용자의 아이디와 비밀번
|
||||
|
||||
### 5. 클라이언트 종류만으로 브라우저가 토큰을 받는지가 정해지지는 않는다
|
||||
|
||||
confidential client가 authorization code를 토큰으로 교환하더라도, 그렇게 받은 액세스 토큰을 다시 브라우저에 전달하는 구조를 만들 수 있다.
|
||||
confidential client가 authorization code를 교환해도 액세스 토큰을 브라우저에 다시 전달할 수 있다. AP2 mediator는 브라우저가 Resource Server를 직접 호출하기 때문에 액세스 토큰을 응답으로 내보낸다.
|
||||
|
||||
클라이언트 종류는 클라이언트 시크릿을 어디에 안전하게 보관할 수 있는지를 말한다.
|
||||
액세스 토큰이 브라우저까지 가는지는 어느 계층이 실제 API 호출을 맡도록 설계했는지에 따라 따로 정해진다.
|
||||
|
||||
이 기준으로 등록한 클라이언트 넷 중 셋이 confidential인데, 그 셋에서 액세스 토큰이 브라우저까지 가는지는 갈렸다.
|
||||
mediator 구성에서는 브라우저가 API를 직접 불러서 액세스 토큰이 응답으로 내려갔고, BFF와 프록시 구성에서는 서버 쪽이 API를 불러서 브라우저에 줄 것이 없었다.
|
||||
AP3 BFF와 AP4 프록시는 다음 API 요청을 서버 쪽에서 만들기 때문에 브라우저에 액세스 토큰을 줄 필요가 없다. 세 구조가 모두 confidential client라는 사실만으로 이 차이는 설명되지 않는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
|
||||
+1
-1
@@ -107,5 +107,5 @@ Mediator나 BFF처럼 서버가 애플리케이션 세션과 Authorized Client
|
||||
- 리프레시 토큰 : 새 액세스 토큰을 받는 장기 자격 증명이다
|
||||
- 애플리케이션 세션 쿠키 : 서버에 있는 로그인 상태를 찾는 자격 증명이다
|
||||
- 프록시 세션 쿠키 : 프록시의 인증 엔드포인트에 제시하는 최소 상태다
|
||||
- CSRF 토큰 : 쿠키가 자동으로 붙는 상태 변경 요청의 의도를 확인한다
|
||||
- CSRF 토큰 : 쿠키가 자동으로 붙는 상태 변경 요청에 별도 검증 값을 요구해 cross-site forged request를 구분한다
|
||||
- 사용자 정보 헤더 : 엣지가 확인한 사용자 정보이고 JWT 토큰이 아니다
|
||||
|
||||
Reference in New Issue
Block a user