feat: 문서 구조 변경 및 tech-visual 스킬 추가
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
id: 39fdf472-82c4-43ed-abec-73de672f08ae
|
||||
kind: REFERENCE
|
||||
slug: authorization-code-endpoint-credential-movement
|
||||
title: Authorization Code Flow의 Endpoint와 Credential 이동 기준
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 34
|
||||
verifiedOn: 2026-08-25
|
||||
studio: "https://hyeonworks.com/studio/documents/39fdf472-82c4-43ed-abec-73de672f08ae/edit"
|
||||
public: "https://hyeonworks.com/references/authorization-code-endpoint-credential-movement"
|
||||
---
|
||||
|
||||
# Authorization Code Flow의 Endpoint와 Credential 이동 기준
|
||||
|
||||
Authorization Endpoint에서 Redirect, Token Endpoint, JWK 검증, Resource API까지 각 지점에서 무엇이 이동하고 무엇이 이동하지 않는지 확인한다. 먼저 client_secret이 가는 곳과 가지 않는 곳을 나눈다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
||||
브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다.
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
confidential client가 token endpoint에서 자기 client를 인증하는 실례다.
|
||||
- **Public Client와 Confidential Client 구분 기준**
|
||||
client 종류가 정해져야 PKCE와 client 인증의 자리가 정해진다.
|
||||
|
||||
## 목적
|
||||
|
||||
Authorization Endpoint와 Token Endpoint는 역할과 호출 방식이 다르다.
|
||||
이 구분을 해야 SPA에서 client_secret이 어디로 갔는지, PKCE가 어느 구간을 지키는지 이해하기 쉽다.
|
||||
|
||||
하나는 브라우저의 full-page navigation이고 하나는 server-to-server 호출이 될 수도 있고 browser-to-server 호출이 될 수도 있다.
|
||||
노출되는 것도, 인증하는 방법도 다르다.
|
||||
|
||||
Authorization Endpoint
|
||||
경로 : 브라우저 주소창 남는 곳 : 히스토리·서버 로그·referrer client 인증 : x
|
||||
|
||||
Token Endpoint
|
||||
경로 : body와 Authorization 헤더 보내는 쪽 : client 종류에 따라 server 또는 브라우저 client 인증 : o
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. Authorization Endpoint에는 client_secret을 보내지 않는다
|
||||
|
||||
이 요청은 브라우저 주소창을 통해 나간다. 그래서 URL이 주소창에도, 브라우저 히스토리에도, 서버 접근 로그에도, 그리고 링크를 타고 온 경우 referrer에도 남는다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. secret이 필요한 인증은 아직 하지 않는다.
|
||||
|
||||
반대로 말하면 이 목록에 없는 값을 여기 넣으면 그 값도 같은 곳에 다 남는다.
|
||||
|
||||
### 2. Token Endpoint에서 비로소 client를 인증한다
|
||||
|
||||
code를 access token으로 바꾸는 요청은 credential을 URL query가 아니라 body와 Authorization 헤더에 싣는다. 그래서 client 인증을 여기서 한다. confidential client는 client_secret_basic처럼 secret을 함께 보낸다.
|
||||
|
||||
주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 debug 로그, reverse proxy 로그, tracing과 APM, packet capture에 남을 수 있어서 credential masking을 따로 둔다.
|
||||
|
||||
이 요청을 누가 보내는지는 client 종류에 따라 갈린다. server가 보내면 server-to-server이고, secret이 없는 SPA가 보내면 브라우저가 직접 보낸다. token endpoint를 server 안에서만 부르게 하려면 client 종류부터 confidential로 정해야 한다.
|
||||
|
||||
### 3. PKCE는 두 요청을 같은 주체에 묶는다
|
||||
|
||||
처음 요청에 code_challenge를 담아 보내고, 교환할 때 원본인 code_verifier를 보낸다.
|
||||
Authorization Server가 이 둘이 대응하는지 확인하고, 대응해야 토큰 교환이 끝난다.
|
||||
|
||||
code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
|
||||
|
||||
### 4. issuer 검증값과 JWK 조회 주소를 같은 값으로 맞추려 하지 않는다
|
||||
|
||||
issuer는 요청을 보내는 주소가 아니라 token의 canonical issuer identifier다. 검증은 발급된 token의 iss claim이 그 값과 같은지를 본다.
|
||||
|
||||
JWK 조회 주소는 실제로 공개키를 가져오는 network 경로다. 이 예제에서는 브라우저가 보는 주소와 컨테이너 안에서 닿는 주소가 다르다. 컨테이너 안에서는 자기 localhost가 그 서버가 아니므로 service 이름을 써야 하고, 브라우저는 그 이름에 닿지 못한다.
|
||||
|
||||
issuer 검증값과 endpoint 연결 주소는 따로 구성한다. 둘을 하나로 맞추려 하면 로그인 redirect가 깨지거나 서버가 키를 못 가져온다.
|
||||
|
||||
### 5. Resource API는 서명만 보고 끝내지 않는다
|
||||
|
||||
서명이 맞다는 것은 그 IdP가 발급했다는 뜻일 뿐이다. 같은 IdP가 다른 API용으로 발급한 token도 서명은 맞다.
|
||||
|
||||
그래서 issuer와 유효 시간, 그리고 이 API를 위해 발급됐다는 audience를 함께 본다. audience 검증이 빠지면 옆 서비스의 token으로 우리 API가 열린다.
|
||||
|
||||
### 6. redirect_uri는 exact match로 좁힌다
|
||||
|
||||
wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른 경로로도 code가 갈 수 있다.
|
||||
|
||||
실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다.
|
||||
|
||||
등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
|
||||
|
||||
### 7. 로그인 구간과 API 호출 구간을 한 줄로 그리지 않는다
|
||||
|
||||
로그인 구간은 authorization request에서 시작해 callback과 code 교환을 지나 로그인 상태를 만드는 데까지다. API 호출 구간은 브라우저 입력, 중간 계층의 credential 변환, 보호 자원의 검증, 최종 응답이다.
|
||||
|
||||
두 구간을 한 줄로 이어 그리면 누가 code를 바꾸고 누가 API를 부르는지가 겹쳐 보인다. 네 구조가 갈리는 자리가 여기라서 나눠서 그린다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- Authorization Code Flow를 쓰는 client를 설정하거나 문서로 설명할 때
|
||||
- 브라우저 요청과 server-to-server 요청이 한 흐름에 섞여 있을 때
|
||||
- endpoint별로 무엇이 노출되는지 나눠야 할 때
|
||||
- PKCE와 client 인증의 자리를 정할 때
|
||||
|
||||
## 예외
|
||||
|
||||
- Client Credentials처럼 사용자 없이 token을 받는 흐름은 Authorization Endpoint를 지나지 않는다.
|
||||
- Device Authorization Grant는 브라우저 redirect 대신 별도의 사용자 code 단계를 쓴다. redirect_uri 항목이 그대로 적용되지 않는다.
|
||||
|
||||
## 예시
|
||||
|
||||
- authorization request에는 code_challenge_method=S256이 있고 client secret은 없다
|
||||
- token request에는 code_verifier가 있다. secret을 가진 client는 이 요청에서 자기를 인증한다
|
||||
- expected issuer는 http://localhost:8080/realms/keycloak-patterns 이고 JWK 조회는 컨테이너 network 주소를 쓴다
|
||||
- audience에 keycloak-pattern-api 가 없으면 invalid_token 결과가 되어 401이 된다
|
||||
- redirect allowlist에 wildcard가 있으면 등록한 host의 다른 경로로도 code가 갈 수 있다
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
---
|
||||
id: 97eddd97-1096-426a-a2c6-a6c5bf1cd09f
|
||||
kind: REFERENCE
|
||||
slug: bff-authentication-design-criteria
|
||||
title: BFF 인증 구조 설계 기준
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 21
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/97eddd97-1096-426a-a2c6-a6c5bf1cd09f/edit"
|
||||
public: "https://hyeonworks.com/references/bff-authentication-design-criteria"
|
||||
---
|
||||
|
||||
# BFF 인증 구조 설계 기준
|
||||
|
||||
BFF 구조에서는 OAuth Token을 서버에서 관리하고, 브라우저는 Token 대신 Session Cookie를 사용해 BFF에 요청한다.
|
||||
|
||||
Cookie를 이용한 요청을 보호하기 위한 CSRF 검증, OAuth Token을 보관할 Authorized Client 저장소, 로그아웃할 때 Session과 Token을 함께 정리하는 방법, 그리고 BFF가 호출한 Resource Server에서 오류가 발생했을 때 이를 브라우저에 어떻게 전달할지를 같이 설계해야 한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
|
||||
이 기준의 항목 중 실제로 구현된 것과 비어 있는 것을 센 기록이다.
|
||||
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
|
||||
저장소 항목이 아직 답이 없는 질문으로 남아 있다.
|
||||
- **BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가**
|
||||
어느 저장소에 둘지가 이 기준의 미결 항목이다.
|
||||
- **BFF가 OAuth Token을 관리하는 조건**
|
||||
이 결정이 PROPOSED인 동안 실제 적용 기준은 이 문서다.
|
||||
|
||||
## 목적
|
||||
|
||||
BFF 구조에서는 BFF가 authorization code를 token으로 교환하고, access token을 사용해 Resource Server를 호출한다.
|
||||
따라서 session과 authorized client를 함께 관리해야 한다.
|
||||
|
||||
cookie가 credential이 되면 브라우저가 요청마다 자동으로 붙여 보낸다. 그래서 값을 바꾸는 요청은 사용자의 의도인지 따로 확인해야 한다. 재시작과 replica 이동을 견딜 저장소도 같이 필요하다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 브라우저에는 OAuth token을 전달하지 않는다
|
||||
|
||||
Access Token과 Refresh Token은 BFF 서버의 Authorized Client에 보관한다.
|
||||
브라우저는 OAuth Token을 직접 사용하지 않고 Session Cookie를 이용해 BFF에 요청한다.
|
||||
|
||||
BFF는 이 Session을 확인한 뒤, 저장해 둔 Access Token으로 `Authorization: Bearer ...` 헤더를 새로 만들어 Resource Server를 호출한다. 브라우저가 보낸 Session Cookie는 Resource Server로 전달되지 않는다. Session Cookie는 브라우저와 BFF 사이의 Credential이고, Access Token은 BFF와 Resource Server 사이의 Credential이다.
|
||||
|
||||
### 2. cookie가 credential이면 상태 변경 요청에 CSRF 검증을 둔다
|
||||
|
||||
BFF 구조에서는 브라우저가 요청할 때 Session Cookie를 자동으로 전송한다.
|
||||
그래서 데이터 생성, 수정, 삭제처럼 서버의 상태를 변경하는 요청에는 해당 요청이 실제 사용자의 의도에 의해 만들어졌는지 확인하기 위한 CSRF 검증이 필요하다.
|
||||
|
||||
현재 구성에서는 서버가 CSRF Token을 Cookie로 전달하고, JavaScript가 그 값을 읽어 요청 Header에 다시 담아 보낸다.
|
||||
서버는 Cookie와 Header를 함께 확인해 요청을 검증한다.
|
||||
|
||||
이때 화면이나 응답 본문에 표시되는 CSRF Token과 실제 요청 Header에 넣어야 하는 값이 항상 같다고 생각하면 안 된다.
|
||||
응답 본문에 노출된 값이 별도의 처리를 거친 값이라면, 클라이언트는 실제 CSRF Cookie에서 값을 읽어 Header에 넣어야 한다.
|
||||
잘못된 값을 보내면 정상적인 요청이라도 CSRF 검증에 실패해 `403 Forbidden` 응답을 받게 된다.
|
||||
|
||||
`SameSite`와 CSRF Token도 서로 다른 역할을 한다.
|
||||
`SameSite`는 브라우저가 Cross-Site 요청에 Cookie를 전송할지 제한하는 정책이고, CSRF Token은 Cookie가 포함되어 들어온 상태 변경 요청이 정상적인 클라이언트에서 만들어졌는지를 확인하기 위한 값이다.
|
||||
또한 SameSite는 Origin이 아니라 Site를 기준으로 판단하므로, Origin은 다르지만 같은 Site에 속하는 요청도 존재할 수 있다.
|
||||
|
||||
### 3. session과 authorized client의 수명주기를 따로 설계한다
|
||||
|
||||
Application Session과 Authorized Client는 서로 다른 값을 저장하고 조회한다.
|
||||
Session은 `session ID`를 기준으로 조회하지만, Authorized Client는 `client registration 이름`과 `principal name`을 기준으로 조회한다.
|
||||
따라서 여러 인스턴스에서 상태를 공유하기 위해 Shared Store를 도입할 때도 Session 저장소와 Authorized Client 저장소를 각각 어떻게 구성할지 확인해야 한다.
|
||||
|
||||
특히 Authorized Client의 조회 기준에는 `session ID`가 포함되지 않는다.
|
||||
그래서 같은 사용자가 두 브라우저에서 동일한 Client로 로그인하면 두 Session이 같은 Authorized Client 정보를 사용하거나, 나중에 로그인하면서 저장된 Token 정보가 갱신될 수 있다.
|
||||
브라우저나 Session마다 서로 다른 Token을 유지해야 한다면 `session ID`까지 포함해 Token을 구분할 수 있도록 별도의 저장 구조를 설계해야 한다.
|
||||
|
||||
운영 환경에서는 서버가 재시작되거나 요청이 다른 Replica로 전달되더라도 로그인 상태와 Token을 계속 사용할 수 있는지도 고려해야 한다. 이를 위해 Session과 Authorized Client를 공유 저장소에 보관할지, Session Affinity를 사용할지 등을 결정해야 한다.
|
||||
Token을 외부 저장소에 보관한다면 Access Token과 Refresh Token을 어떻게 보호할지도 정해야 하며, 저장 시 암호화한다면 암호화 Key의 보관 위치와 교체 방법까지 함께 설계해야 한다.
|
||||
|
||||
Logout에서도 두 상태를 각각 정리해야 한다.
|
||||
Application Session을 삭제하는 것만으로 Authorized Client에 저장된 OAuth Token까지 자동으로 삭제된다고 생각하면 안 된다.
|
||||
Session과 Authorized Client는 조회 기준과 저장소가 다르므로, Logout 시 Session과 Authorized Client가 모두 제거되는지 각각 확인해야 한다.
|
||||
|
||||
### 4. Downstream 오류를 클라이언트 응답으로 변환한다
|
||||
|
||||
BFF가 Resource Server의 오류를 그대로 브라우저에 전달하면 화면에서는 오류의 원인을 일관되게 판단하기 힘들다.
|
||||
예를 들어 Resource Server에서 `401 Unauthorized`가 발생했다면 Access Token이 만료되었거나 더 이상 유효하지 않은 상황인지 확인하고, 필요한 경우 Token 갱신이나 재로그인으로 연결해야 한다.
|
||||
하지만 인증은 정상적으로 되었지만 해당 기능을 사용할 권한이 없어 `403 Forbidden`이 발생한 경우에는 권한 부족으로 처리해야 한다.
|
||||
|
||||
Resource Server가 응답하지 않거나 처리가 지연되는 경우도 별도의 규칙이 필요하다.
|
||||
요청을 얼마 동안 기다릴지 Timeout을 정하고, 실패한 요청을 다시 시도할 수 있는 경우에는 Retry 정책을 적용한다.
|
||||
반복적으로 장애가 발생하는 Resource Server에 계속 요청을 보내지 않도록 Circuit Breaker를 적용할지도 함께 결정한다.
|
||||
|
||||
모든 UI 요청이 BFF를 거치는 구조라면 이러한 오류 처리 규칙도 BFF에서 일관되게 적용하는 것이 좋다.
|
||||
그렇지 않으면 같은 종류의 오류를 화면마다 서로 다른 방식으로 판단하고 처리하게 될 수 있다.
|
||||
|
||||
### 5. BFF에서도 XSS 방어는 별도로 필요하다
|
||||
|
||||
BFF 구조에서는 Access Token과 Refresh Token을 서버에 보관하므로 브라우저의 JavaScript가 OAuth Token 원문에 직접 접근하지 않도록 할 수 있다. 하지만 이것이 브라우저에서 실행되는 악성 JavaScript까지 막아 주는 것은 아니다.
|
||||
|
||||
같은 Origin에서 악성 Script가 실행되면 사용자의 Session을 이용해 BFF Endpoint를 호출할 수 있다.
|
||||
현재처럼 JavaScript가 CSRF Cookie를 읽어 Header에 넣는 구조라면 악성 Script 역시 같은 방식으로 CSRF Token을 읽어 요청을 만들 수 있다.
|
||||
|
||||
BFF에서는 OAuth Token 원문이 브라우저 JavaScript에 직접 노출되지 않지만, XSS 자체를 방지하기 위한 CSP, Output Encoding 등의 보호 조치와 외부 Script 및 의존성을 안전하게 관리하는 방법은 별도로 적용해야 한다.
|
||||
또한 악성 Script가 사용자의 Session을 이용해 BFF를 호출하더라도 허용된 작업만 수행할 수 있도록 애플리케이션의 인가 역시 각 요청에서 검증해야 한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 브라우저가 OAuth token을 받아서는 안 될 때
|
||||
- backend가 화면에 맞춰 여러 API를 조합해야 할 때
|
||||
- 로그인 상태를 애플리케이션이 소유해야 할 때
|
||||
- downstream API가 늘어나도 브라우저는 하나만 알게 하고 싶을 때
|
||||
|
||||
## 예외
|
||||
|
||||
- stateless 직접 API 호출과 독립 client가 핵심이면 BFF 구조로 설계하지 않는다.
|
||||
server state와 단일 장애 지점만 늘어난다.
|
||||
- 브라우저의 직접 API 호출을 남겨야 하면 refresh credential만 서버로 분리하는 구조가 맞다.
|
||||
- server state를 둘 수 없는 환경이면 브라우저가 token을 직접 다루는 구조가 더 단순하다.
|
||||
|
||||
## 예시
|
||||
|
||||
- 브라우저 요청에는 Authorization 헤더가 없고 session cookie만 있다
|
||||
- BFF가 authorized client에서 access token을 읽어 downstream Bearer 요청을 새로 만든다
|
||||
- CSRF 헤더가 없는 POST는 403이 되고 cookie의 raw 값을 헤더에 넣은 POST는 200이 된다
|
||||
- 응답 본문의 token은 가려진 값이고 헤더에 넣는 값은 cookie의 raw 값이다
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
---
|
||||
id: 004dd0a2-5fb3-4f25-80c9-576f709de331
|
||||
kind: REFERENCE
|
||||
slug: forward-auth-identity-header-trust
|
||||
title: Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 29
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/004dd0a2-5fb3-4f25-80c9-576f709de331/edit"
|
||||
public: "https://hyeonworks.com/references/forward-auth-identity-header-trust"
|
||||
---
|
||||
|
||||
# Forward-Auth에서 Identity Header를 신뢰하기 위한 조건
|
||||
|
||||
애플리케이션이 프록시가 전달한 사용자 정보 헤더만으로 사용자를 판단하는 구조에서는, 해당 헤더가 실제로 신뢰할 수 있는 프록시에서 전달되었다는 것을 보장해야 한다.
|
||||
|
||||
이를 위해 외부 사용자가 애플리케이션에 직접 접근하지 못하도록 네트워크 경로를 제한하고,
|
||||
사용자가 같은 이름의 헤더를 임의로 보내더라도 프록시가 이를 제거하거나 올바른 값으로 덮어써야 한다.
|
||||
또한 필요한 경우 프록시에서 전달된 요청임을 확인할 수 있는 내부용 Credential도 함께 검증한다.
|
||||
|
||||
세 가지는 각각 다른 구간을 막으므로 하나만 적용하지 않고 같이 구성한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
|
||||
앞에서 정리한 다섯 가지 조건이 실제 설정에 적용되어 있는지 확인한 결과.
|
||||
- **Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가**
|
||||
헤더를 어디까지 늘릴지가 이 기준의 미결 항목이다.
|
||||
- **OAuth Token과 Application Session을 구분하는 기준**
|
||||
identity 헤더를 JWT나 session과 같은 이름으로 부르지 않는다.
|
||||
|
||||
## 목적
|
||||
|
||||
외부 요청이 인증 프록시를 거쳐 애플리케이션으로 전달되는 구조에서는, 애플리케이션이 프록시가 추가한 사용자 정보 헤더를 기준으로 로그인한 사용자를 판단할 수 있다.
|
||||
|
||||
문제는 같은 이름의 헤더를 외부 사용자가 직접 만들어서 보낼 수도 있다는 점이다.
|
||||
애플리케이션 입장에서는 전달받은 헤더만 보고 이것이 인증을 완료한 프록시가 추가한 값인지, 외부 사용자가 임의로 넣은 값인지 구분할 수 없다.
|
||||
|
||||
그래서 사용자 정보 헤더를 인증 근거로 사용하려면 먼저 외부 요청이 반드시 인증 프록시를 거쳐서만 애플리케이션에 도달하도록 구성해야 한다. 또한 애플리케이션이 받은 요청과 헤더가 신뢰할 수 있는 프록시를 통해 전달된 것인지 확인할 수 있는 방법도 같이 생각해야 한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 외부에서 애플리케이션과 인증 프록시에 직접 접근하지 못하게 한다
|
||||
|
||||
외부에서는 Edge에만 접근할 수 있도록 하고, 애플리케이션과 인증 프록시는 내부 네트워크에만 두어 Host Port로 직접 노출하지 않는다.
|
||||
|
||||
애플리케이션이 외부에 직접 노출되어 있으면 공격자가 Edge의 인증 과정을 거치지 않고 애플리케이션으로 요청을 보낼 수 있다.
|
||||
이 경우 공격자가 사용자 정보 헤더까지 직접 만들어 보낼 수 있으므로, 애플리케이션은 해당 헤더가 인증을 거쳐 생성된 값인지 신뢰할 수 없게 된다.
|
||||
그래서 사용자 정보 헤더를 인증 근거로 사용하려면 먼저 모든 외부 요청이 반드시 Edge를 거치도록 네트워크 경로부터 제한해야 한다.
|
||||
|
||||
### 2. client가 보낸 헤더를 항상 덮어쓴다
|
||||
|
||||
사용자 정보 헤더는 외부 요청에 들어 있던 값과 합치지 않고, 인증 프록시가 확인한 값으로 기존 헤더를 제거하거나 덮어쓴 뒤 애플리케이션에 전달한다.
|
||||
|
||||
기존 헤더와 인증 결과를 합쳐서 전달하면 공격자가 넣은 값과 프록시가 추가한 값이 하나의 헤더에 함께 포함될 수 있다.
|
||||
이때 애플리케이션이 어떤 값을 사용자 정보로 사용할지는 헤더 처리 방식에 따라 달라질 수 있으므로, 인증된 값만 전달되도록 해야 한다.
|
||||
|
||||
또한 신뢰할 수 있는 프록시의 범위도 필요한 대상만 포함하도록 제한한다.
|
||||
이 범위를 너무 넓게 설정하면 같은 내부 네트워크에 있는 다른 서비스가 신뢰받는 프록시처럼 요청을 보낼 수 있다.
|
||||
특히 `Forwarded`나 `X-Forwarded-*` 헤더를 신뢰하는 구조에서는 어떤 프록시의 요청까지 신뢰할지를 먼저 좁혀 둔다.
|
||||
|
||||
### 3. auth endpoint는 subrequest 전용으로 둔다
|
||||
|
||||
이 Endpoint는 외부 사용자가 직접 호출하는 API가 아니라, 인증 과정에서 Proxy가 내부적으로 호출하기 위한 Endpoint다.
|
||||
따라서 외부 요청으로는 접근할 수 없게 하고 Proxy가 생성한 내부 요청만 허용해야 한다.
|
||||
|
||||
Nginx에서는 해당 Location에 `internal`을 설정해 외부에서 직접 호출하는 것을 차단할 수 있다.
|
||||
|
||||
### 4. upstream이 헤더 존재만 보지 않는다
|
||||
|
||||
요청이 신뢰할 수 있는 Proxy에서 전달된 것인지 확인하기 위해, 배포할 때 설정한 내부용 Credential과 요청에 포함된 Credential을 비교한다. 이때 Credential 값의 일부가 얼마나 일치하는지에 따라 비교 시간이 크게 달라지지 않는 안전한 비교 방식을 사용한다.
|
||||
|
||||
이 검증을 각 Controller에서 개별적으로 처리하면 새로운 Endpoint를 추가할 때 검증 로직을 빠뜨릴 수 있기 때문에 운영 환경에서는 `Filter`, `Interceptor`, `Security Chain`과 같은 공통 처리 지점에서 모든 대상 요청에 동일한 검증이 적용되도록 구성해야 한다.
|
||||
|
||||
### 5. Network 격리와 헤더 검증을 모두 적용한다
|
||||
|
||||
네트워크 격리는 외부 사용자가 인증 경로를 우회해 애플리케이션에 직접 접근하는 것을 막는다.
|
||||
헤더 검증은 내부 네트워크에서 전달된 요청이라도 사용자 정보 헤더가 신뢰할 수 있는 값인지 확인한다.
|
||||
|
||||
두 방식은 보호하는 구간과 대상이 다르므로 둘 다 구성한다.
|
||||
|
||||
### 6. 인증된 사용자 정보 헤더만 전달한다
|
||||
|
||||
인증 프록시가 애플리케이션으로 전달할 사용자 정보 헤더를 미리 정해 두고, 허용하지 않은 헤더는 전달하지 않는다.
|
||||
새로운 헤더를 추가할 때는 해당 값이 어떤 Claim에서 만들어지는지, 여러 값이 있을 때 어떤 형식으로 전달할지, 특수 문자를 어떻게 처리할지, 허용할 최대 크기는 얼마인지, 애플리케이션에서는 그 값을 어떻게 검증하고 사용할지를 함께 정해야 한다.
|
||||
|
||||
현재처럼 사용자 이름과 이메일만 전달하는 구조에서는 로그인한 사용자가 누구인지는 알 수 있지만, 해당 사용자가 어떤 권한을 가지고 있는지까지 알 수는 없다.
|
||||
Role을 이용해 인가까지 처리하려면 Role 정보를 어떤 방식으로 전달할지뿐만 아니라, 사용자의 Role이 변경되었을 때 기존 Proxy Session과 애플리케이션의 인가 결과에 언제 반영할지도 별도로 정해야 한다.
|
||||
|
||||
### 7. 요청 성공 여부가 아니라 전달된 사용자 정보를 확인한다
|
||||
|
||||
정상적으로 로그인된 세션에서 사용자 정보 헤더만 위조해 요청했다면, 세션 자체는 유효하므로 요청이 `200 OK`로 처리되는 것은 정상이다.
|
||||
|
||||
테스트에서 확인해야 하는 것은 요청의 성공이나 실패가 아니라 애플리케이션이 어떤 사용자를 인증된 사용자로 인식했는지다.
|
||||
공격자가 임의로 넣은 사용자 정보가 아니라, 인증 프록시가 확인한 실제 사용자 정보가 사용되어야 한다.
|
||||
응답 코드만으로는 알 수 없으므로 실제 응답에 사용된 사용자 정보까지 확인한다.
|
||||
|
||||
### 8. 지금 확인한 것과 운영에서 더 필요한 것을 나눠 적는다
|
||||
|
||||
현재 테스트 환경에서는 외부에서 애플리케이션으로 직접 접근할 수 없는지,
|
||||
외부 사용자가 넣은 사용자 정보 헤더를 인증된 값으로 덮어쓰는지,
|
||||
인증 Endpoint를 내부 요청으로만 호출할 수 있는지,
|
||||
그리고 애플리케이션이 내부 Credential을 검증하는지까지 확인했다.
|
||||
|
||||
다만 실제 운영 환경에서는 추가적인 보안 구성이 필요하다.
|
||||
내부 Credential과 같은 Secret은 Secret Manager 등을 통해 안전하게 주입하고 주기적으로 교체할 수 있어야 한다.
|
||||
또한 Network Policy 등을 이용해 모든 요청이 정해진 인증 경로를 거치도록 제한해야 한다.
|
||||
더 강한 서비스 간 인증이 필요하다면 mTLS나 Workload Identity를 적용하는 방법도 고려할 수 있다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- upstream에 OAuth client나 JWT 검증 코드를 넣기 어려울 때
|
||||
- 여러 legacy service 앞에 같은 로그인 정책을 둘 때
|
||||
- edge에서 정책을 강제할 수 있을 때
|
||||
- 이미 forward-auth를 쓰고 있는 구조를 점검할 때
|
||||
|
||||
## 예외
|
||||
|
||||
- backend 직접 경로나 헤더 덮어쓰기를 닫을 수 없는 환경이면 이 구조를 쓰지 않는다.
|
||||
- 애플리케이션이 사용자별 API 조합과 세밀한 인가를 직접 맡아야 하면 BFF 구조를 검토한다.
|
||||
- 임의 경로와 body, streaming을 그대로 넘기는 범용 reverse proxy가 필요하면 URI rewrite와 timeout, 응답 헤더 처리를 따로 설계해야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
- 외부에는 edge만 공개하고 app과 auth proxy의 port는 host에 publish하지 않는다
|
||||
- 정상 session에 위조 헤더를 얹은 요청은 200을 받지만 응답의 사용자는 실제 사용자다
|
||||
- 외부에서 auth endpoint를 직접 부르면 404가 된다
|
||||
- upstream은 user 헤더와 internal token을 같이 확인하고 하나라도 틀리면 401을 반환한다
|
||||
- 내부 검사가 특정 controller에만 있으면 새 endpoint에는 보호되지 않는다
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: 1a00a640-8987-4075-a9e4-7ec023cdffbb
|
||||
kind: REFERENCE
|
||||
slug: external-idp-federation-application-boundary
|
||||
title: 외부 IdP 연동과 Application 인증 구조의 경계
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 27
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/1a00a640-8987-4075-a9e4-7ec023cdffbb/edit"
|
||||
public: "https://hyeonworks.com/references/external-idp-federation-application-boundary"
|
||||
---
|
||||
|
||||
# 외부 IdP 연동과 Application 인증 구조의 경계
|
||||
|
||||
Google 로그인을 추가한다고 해서 새로운 다섯 번째 인증 구조가 생기는 것은 아니다.
|
||||
사용자가 Google에서 인증을 마치면 Keycloak이 그 인증 결과를 받아 사용자를 확인하고, 애플리케이션에는 자신의 Authorization Code를 발급한다.
|
||||
|
||||
그 이후의 흐름은 기존과 같다. 애플리케이션은 여전히 Keycloak을 기준으로 인증을 처리하고, Token을 브라우저에서 관리할지 서버에서 관리할지에 따라 앞에서 구분한 네 가지 구조 중 하나를 사용한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **외부 IdP와의 연동이라도 별도의 인증 방식이 아니다.**
|
||||
이 기준을 프로젝트 결정으로 굳힌 기록이다.
|
||||
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
||||
브로커가 만든 authorization code를 애플리케이션이 받는 흐름이다.
|
||||
- **Authorization Code Flow의 Endpoint와 Credential 이동 기준**
|
||||
외부 IdP가 있어도 애플리케이션 쪽 endpoint 이동은 그대로다.
|
||||
|
||||
## 목적
|
||||
|
||||
Google은 Keycloak 앞에서 실제 사용자 인증을 담당하는 외부 IdP다.
|
||||
사용자가 Keycloak 로그인 화면에서 Google을 선택하면 브라우저는 Google로 이동해 인증을 진행한다.
|
||||
인증이 완료되면 Keycloak이 그 결과를 확인하고 자신의 사용자 정보와 연결한 뒤, 애플리케이션에는 Keycloak이 발급한 Authorization Code를 전달한다.
|
||||
|
||||
따라서 Google과 같은 외부 IdP를 추가하더라도 애플리케이션의 인증 구조가 달라지는 것은 아니다.
|
||||
Token을 브라우저가 직접 받을지 서버에서 관리할지, 그리고 브라우저와 서버 중 어느 계층이 Resource Server의 API를 호출할지는 기존 SPA, Mediator, BFF, OAuth2-Proxy 구조 중 어떤 방식을 선택했는지에 따라 결정된다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 외부 IdP 인증과 애플리케이션 인증 구조를 나눈다
|
||||
|
||||
외부 IdP는 Keycloak 앞에서 사용자 인증을 담당한다. 애플리케이션이 선택하는 SPA, Mediator, BFF, OAuth2-Proxy 구조는 Keycloak에서 인증이 끝난 이후 Token과 API 호출을 어떻게 처리할지를 정한다.
|
||||
|
||||
Google에서 인증이 완료되면 그 결과는 먼저 Keycloak이 검증한다.
|
||||
이후 애플리케이션은 Google이 아니라 Keycloak이 발급한 Authorization Code와 Token을 사용한다.
|
||||
Resource Server 역시 Keycloak이 발급한 Token을 검증한다.
|
||||
따라서 Google 로그인을 추가하더라도 애플리케이션의 OAuth 처리 방식은 기존 SPA, Mediator, BFF, OAuth2-Proxy 구조를 그대로 따른다.
|
||||
|
||||
로그인 화면에서 Google이나 다른 Provider를 선택하게 하거나, Provider별 계정을 Keycloak 사용자와 어떻게 연결할지를 별도로 처리하는 것은 자연스럽다.
|
||||
하지만 Resource Server가 Google과 Keycloak의 Token을 각각 다르게 검증하거나, 애플리케이션의 인가 로직이 로그인에 사용한 Provider에 따라 달라지기 시작한다면 외부 IdP와 애플리케이션 사이를 분리하던 Keycloak의 역할이 제대로 유지되고 있는지 확인할 필요가 있다.
|
||||
|
||||
### 2. 외부 계정은 provider와 `subject` 조합으로 식별한다
|
||||
|
||||
이메일 주소는 변경될 수 있고 다른 계정과 중복될 가능성도 있기 때문에 외부 계정을 식별하고 연결하는 기준으로 사용하기에는 적절하지 않다.
|
||||
|
||||
대신 어떤 Provider에서 인증했는지와 해당 Provider가 사용자에게 부여한 고유 식별자(`subject`)를 함께 사용해 외부 계정을 식별한다.
|
||||
예를 들어 Google 사용자는 `Google + subject`의 조합으로 구분한다.
|
||||
|
||||
이메일만을 기준으로 계정을 연결하면 사용자가 이메일 주소를 변경했을 때 기존 계정과의 연결을 찾지 못하거나, 동일한 이메일을 가진 다른 계정을 잘못 연결할 수 있다.
|
||||
|
||||
### 3. email 충돌은 별도의 계정 연결 문제로 다룬다
|
||||
|
||||
외부 IdP에서 전달받은 이메일 주소가 기존 계정의 이메일과 같더라도 두 계정을 자동으로 연결하지 않는다.
|
||||
이메일이 같다는 사실만으로 두 계정이 같은 사용자의 것이라고 확신할 수 없기 때문이다.
|
||||
|
||||
계정을 연결해야 한다면 기존 계정으로 다시 로그인하거나 추가 인증을 요구하는 등, 사용자가 해당 계정의 실제 소유자임을 확인하는 별도의 절차를 거친다.
|
||||
|
||||
### 4. mock provider 테스트와 실제 IdP 검증을 구분한다
|
||||
|
||||
현재는 Mock Provider를 사용해 Keycloak이 외부 IdP의 인증 결과를 정상적으로 받아들이는지와 필요한 사용자 정보가 올바르게 매핑되는지까지 확인했다.
|
||||
|
||||
하지만 Mock Provider 테스트만으로 실제 외부 IdP와의 연동까지 검증할 수는 없다.
|
||||
실제 계정으로 로그인하는 과정과 공개 HTTPS Callback, 사용자 동의(Consent) 화면, 외부 IdP가 적용하는 도메인 정책 등은 아직 확인하지 않았다.
|
||||
그래서 실제 외부 IdP를 연결해 전체 로그인 흐름을 별도로 검증해야 한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 외부 IdP를 붙일 때
|
||||
- 계정 연결 규칙을 정할 때
|
||||
- 검증 범위를 문서로 적을 때
|
||||
- 브로커를 거치는 흐름과 직접 OIDC 흐름을 비교할 때
|
||||
|
||||
## 예외
|
||||
|
||||
- 애플리케이션이 Keycloak과 같은 브로커를 거치지 않고 Google 등의 외부 IdP와 직접 OIDC 연동을 한다면 상황이 달라진다.
|
||||
이 경우 애플리케이션은 외부 IdP가 직접 발급한 Token을 사용하므로, 해당 외부 IdP를 신뢰하고 Token을 검증하게 된다.
|
||||
- 조직에서 하나의 외부 IdP만 사용한다면 Keycloak과 같은 별도의 브로커를 두지 않고 애플리케이션이 해당 IdP와 직접 연동하는 구조도 선택할 수 있다.
|
||||
|
||||
이 경우 여러 외부 IdP에서 들어온 계정을 하나의 내부 사용자와 어떻게 연결할지 결정하는 계정 연결 정책은 대부분 필요 없다.
|
||||
|
||||
## 예시
|
||||
|
||||
- Google 로그인을 추가해도 애플리케이션이 고르는 것은 여전히 4가지 구조 중 하나다
|
||||
- 브로커는 `provider alias + upstream subject` 조합을 기준으로 외부 계정을 식별한다.
|
||||
- 애플리케이션이 신뢰하는 issuer는 외부 IdP가 아니라 브로커다
|
||||
- mock OIDC provider로 확인한 것은 브로커와 claim mapping 계약까지다
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: 3f886154-1b85-407b-bda4-57d28370e745
|
||||
kind: REFERENCE
|
||||
slug: oauth-oidc-pattern-selection-criteria
|
||||
title: OAuth/OIDC 인증 패턴 선택 기준
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 23
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/3f886154-1b85-407b-bda4-57d28370e745/edit"
|
||||
public: "https://hyeonworks.com/references/oauth-oidc-pattern-selection-criteria"
|
||||
---
|
||||
|
||||
# OAuth/OIDC 인증 패턴 선택 기준
|
||||
|
||||
SPA, Mediator, BFF, OAuth2-Proxy는 Token과 인증 상태를 처리하는 방식이 서로 다르다.
|
||||
브라우저가 Access Token을 직접 사용하는지, 실제 Resource Server를 누가 호출하는지, 서버에서 어떤 인증 상태를 보관하는지, Resource Server가 어떤 Credential을 검증하는지, CSRF를 어느 계층에서 처리하는지를 비교할 수 있다.
|
||||
네 구조를 안전한 순서로 줄 세우지 않고, 애플리케이션의 요구사항과 배포·운영 환경에 맞춰 고른다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
||||
브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다.
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
mediator가 refresh token을 관리하고 브라우저가 access token으로 API를 직접 호출하는 구성을 확인했다.
|
||||
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
|
||||
BFF가 code 교환, token 보관, Resource Server 호출을 모두 처리하는 구성을 확인했다.
|
||||
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
|
||||
인증이 edge로 가면 보호 자원이 검증하는 것이 JWT에서 헤더로 바뀐다.
|
||||
|
||||
## 목적
|
||||
|
||||
브라우저에 OAuth Token이 노출되는 정도만 놓고 보면 구조별 차이는 있다.
|
||||
Token을 다른 위치로 옮기면 브라우저에 노출되는 범위가 달라지고, 그 Token을 맡게 된 계층에서 처리해야 할 항목이 늘어난다.
|
||||
|
||||
예를 들어 BFF는 OAuth Token을 서버에 보관해 브라우저에서 Token 원문을 제거할 수 있다.
|
||||
하지만 서버가 Session과 Authorized Client를 관리해야 하므로 Session 보호, CSRF 방어, 공유 저장소와 같은 새로운 설계가 필요해진다.
|
||||
|
||||
Forward-Auth 구조에서는 애플리케이션이 OAuth Token을 직접 관리하는 책임을 더 줄일 수 있다.
|
||||
대신 애플리케이션이 Edge에서 전달된 사용자 정보 헤더를 신뢰하게 되므로, 헤더 위조 방지와 직접 접근 차단, 신뢰할 수 있는 네트워크 경계를 구성해야 한다.
|
||||
|
||||
그래서 어느 구조가 더 안전한지를 먼저 정하지 않고, 요구사항별로 무엇을 확인해야 하는지를 본다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 다섯 항목으로 구조를 비교한다
|
||||
|
||||
구조를 비교할 때는 브라우저의 Access Token 사용 여부, Resource Server 호출 주체, 서버에서 관리하는 인증 상태, Resource Server가 검증하는 Credential, CSRF 처리 위치를 확인한다.
|
||||
|
||||
SPA는 Bearer Access Token을 직접 `Authorization` Header에 넣어 Resource Server를 호출하고, 인증에 Cookie를 사용하지 않는다.
|
||||
|
||||
SPA와 Mediator에서는 브라우저가 Access Token을 사용해 Resource Server를 직접 호출한다.
|
||||
차이는 Mediator가 로그인 Session과 OAuth Token을 서버에서도 관리하고, 로그인 이후 브라우저에 Access Token을 전달한다는 점이다.
|
||||
|
||||
BFF에서는 브라우저가 Session Cookie로 BFF를 호출하고, BFF가 서버에 저장된 Access Token을 사용해 Resource Server를 호출한다. 따라서 브라우저에는 OAuth Token을 전달하지 않지만 Session과 Authorized Client를 서버에서 관리해야 한다.
|
||||
|
||||
Forward-Auth에서는 인증 Proxy가 Session을 관리하고, 인증이 완료된 요청에 사용자 정보를 추가해 애플리케이션으로 전달한다. 애플리케이션이 이 정보를 인증 근거로 사용한다면 Edge가 전달한 헤더를 신뢰할 수 있도록 직접 접근 차단, 헤더 덮어쓰기, 내부 Credential 검증과 같은 별도의 보호가 필요하다.
|
||||
|
||||
### 2. 피해야 할 조건을 먼저 확인한다
|
||||
|
||||
구조를 비교하기 전에 먼저 반드시 지켜야 하는 보안 요구사항을 확인한다.
|
||||
|
||||
정책상 OAuth Token을 브라우저에 둘 수 없다면 Token을 Local Storage 대신 JavaScript Memory에만 보관하는 것으로는 요구사항을 충족할 수 없다.
|
||||
저장 위치가 달라졌을 뿐 브라우저 JavaScript가 여전히 Token을 직접 다루기 때문이다.
|
||||
이 경우 브라우저가 Access Token을 받는 SPA나 현재의 Mediator 구조는 선택 대상에서 제외한다.
|
||||
|
||||
마찬가지로 애플리케이션에 직접 접근하는 경로를 차단할 수 없거나 외부에서 전달된 사용자 정보 헤더를 Edge에서 확실하게 제거하거나 덮어쓸 수 없다면, Edge가 전달한 사용자 정보를 인증 근거로 사용하는 구조는 선택하지 않는다.
|
||||
|
||||
### 3. 선택 조건과 운영 책임을 같이 문서화한다
|
||||
|
||||
어떤 인증 구조를 선택했는지만 기록하지 않는다. 어떤 보안 요구사항과 운영 조건 때문에 해당 구조를 선택했는지 함께 기록한다.
|
||||
|
||||
또한 해당 구조를 적용하기 어려운 조건도 남긴다.
|
||||
예를 들어 브라우저에 OAuth Token을 둘 수 없는 환경에서는 SPA를 선택하기 어렵고, 애플리케이션의 직접 접근 경로나 사용자 정보 헤더를 안전하게 통제할 수 없는 환경에서는 Forward-Auth 구조를 적용하기 어렵다.
|
||||
|
||||
이렇게 선택 이유와 적용할 수 없는 조건을 함께 기록해야 이후 요구사항이나 운영 환경이 변경되었을 때
|
||||
기존 선택이 여전히 유효한지 다시 판단할 수 있다.
|
||||
|
||||
### 4. 이름으로 운영 속성을 추정하지 않는다
|
||||
|
||||
실제 운영에 적용할 때는 서버가 재시작되거나 특정 인스턴스에 장애가 발생해도 로그인 상태를 유지할 수 있는지,
|
||||
여러 Replica가 필요한 Session과 Token 정보를 공유할 수 있는지,
|
||||
저장소 장애가 발생했을 때 어떻게 복구할지 등을 별도로 확인해야 한다.
|
||||
내부 Credential이나 암호화 Key와 같은 Secret을 안전하게 보관하고 교체할 수 있는지도 함께 검증해야 한다.
|
||||
구조를 고를 때 이런 운영 항목까지 같이 적는다.
|
||||
|
||||
### 5. Credential의 위치가 바뀌면 저장·전달·검증 주체도 바뀐다
|
||||
|
||||
인증 패턴을 변경하면 Credential의 위치만 달라지는 것이 아니라, Credential을 저장하고 전달하고 검증하는 주체도 함께 바뀐다.
|
||||
따라서 패턴을 변경할 때는 기존 책임이 어느 계층으로 이동하는지까지 확인해야 한다.
|
||||
|
||||
예를 들어 Forward-Auth 구조에서는 Edge가 인증된 사용자 정보를 Header로 애플리케이션에 전달할 수 있다.
|
||||
처음에는 사용자 이름이나 이메일처럼 인증에 필요한 정보만 전달하더라도, 애플리케이션의 요구사항이 늘어나면서 Role이나 권한, 도메인에 종속된 사용자 정보까지 Header에 계속 추가될 수 있다.
|
||||
|
||||
이처럼 Edge가 전달해야 하는 정보가 계속 늘어나고 애플리케이션의 인가 판단이나 화면에 필요한 여러 API 응답의 조합까지 필요해진다면,
|
||||
해당 책임을 Edge에 계속 추가하기보다 BFF에서 인가와 API 호출을 처리하는 구조가 더 적절한지 다시 검토한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 인증 구조를 처음 고를 때
|
||||
- 한 구조에서 다른 구조로 옮기려 할 때
|
||||
- 구조를 문서로 비교할 때
|
||||
|
||||
## 예외
|
||||
|
||||
- 요구가 하나로 좁혀지면 비교가 필요 없다. 브라우저에 token을 둘 수 없고 backend가 API를 조합해야 하면 선택지는 하나다.
|
||||
- 학습이나 시연이 목적이면 운영 속성 비교를 하지 않아도 된다.
|
||||
|
||||
## 예시
|
||||
|
||||
- SPA : 브라우저가 code 교환과 token 보관, API 호출을 모두 맡는다
|
||||
- Mediator : refresh token은 server에 있고 access token은 응답 본문으로 브라우저에 반환한다.
|
||||
- BFF : server가 code 교환·token 관리·API 호출을 담당하고 브라우저는 session cookie로 BFF를 호출한다
|
||||
- Forward-Auth : edge가 인증하고 upstream은 edge가 붙인 헤더를 본다
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
id: ede6b9ce-eeed-40c8-9175-9e8116029395
|
||||
kind: REFERENCE
|
||||
slug: public-confidential-client-boundary
|
||||
title: Public Client와 Confidential Client 구분 기준
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 27
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/ede6b9ce-eeed-40c8-9175-9e8116029395/edit"
|
||||
public: "https://hyeonworks.com/references/public-confidential-client-boundary"
|
||||
---
|
||||
|
||||
# Public Client와 Confidential Client 구분 기준
|
||||
|
||||
OAuth Client의 종류는 client secret을 안전하게 보관할 수 있는지를 기준으로 결정한다.
|
||||
SPA는 브라우저에서 실행되기 때문에 secret을 사용자에게 노출하지 않고 안전하게 보관할 수 없다.
|
||||
따라서 SPA는 일반적으로 Public Client로 등록한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
||||
SPA를 public client로 등록한 이유를 실제 구성에서 확인할 수 있다.
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
confidential client를 사용해도 access token 전달 방식은 별도로 설계된다는 예다.
|
||||
- **Authorization Code Flow의 Endpoint와 Credential 이동 기준**
|
||||
client 종류에 따라 token endpoint의 client 인증 방식이 달라진다.
|
||||
|
||||
## 목적
|
||||
|
||||
먼저 OAuth Client가 Public Client인지 Confidential Client인지 결정해야 한다.
|
||||
그래야 Authorization Code를 Token으로 교환할 때 PKCE를 사용할지, client secret을 이용한 Client 인증을 사용할지 결정할 수 있다.
|
||||
|
||||
Client 종류를 나누는 기준은 client secret을 사용자에게 노출하지 않고 안전하게 보관할 수 있는지다.
|
||||
SPA는 브라우저에서 실행되기 때문에 코드에 secret을 넣어도 개발자 도구 등을 통해 사용자가 확인할 수 있다.
|
||||
따라서 SPA는 secret을 안전하게 보관할 수 없는 Public Client로 구성한다.
|
||||
|
||||
반면 서버나 BFF는 secret을 서버 내부에 보관하고 브라우저에 전달하지 않을 수 있으므로 Confidential Client로 구성할 수 있다.
|
||||
|
||||
여기서 Client 종류와 Token을 누가 관리하는지는 구분해야 한다.
|
||||
Confidential Client라고 해서 반드시 Token이 서버에만 있어야 하는 것은 아니다.
|
||||
Client 종류는 secret을 안전하게 보관할 수 있는지로 정하고, Token을 브라우저와 서버 중 어디에서 관리할지는 애플리케이션의 인증 구조에 따라 별도로 정한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. secret을 숨길 수 있는지로 종류를 정한다
|
||||
|
||||
애플리케이션의 배포 파일이나 실행 중인 메모리에서 사용자가 `client secret`을 확인할 수 있다면 이를 안전하게 보관할 수 없으므로 Public Client로 본다.
|
||||
반대로 `client secret`을 서버 내부에만 보관하고 사용자에게 전달되지 않도록 통제할 수 있다면 Confidential Client로 구성할 수 있다.
|
||||
|
||||
Native App도 브라우저에서 실행되는 것은 아니지만 애플리케이션이 사용자 기기에 설치되기 때문에, 배포 파일을 분석하면 내부에 포함된 `client secret`을 확인할 수 있다. 따라서 Native App 역시 일반적으로 Public Client로 다룬다.
|
||||
|
||||
### 2. public client에서 Authorization Code Flow에 PKCE를 함께 쓴다
|
||||
|
||||
PKCE는 `client secret`을 대신해서 Client를 인증하는 방식이 아니다.
|
||||
Authorization Code가 중간에 탈취되더라도 다른 사람이 그 Code를 Token으로 교환하기 어렵게 만드는 보호 장치다.
|
||||
|
||||
로그인을 시작할 때 Client는 임의의 `code_verifier`를 만들고, 이를 변환한 `code_challenge`를 Authorization Request에 함께 보낸다. 이후 Authorization Code를 Token으로 교환할 때 원래의 `code_verifier`를 제출한다.
|
||||
Authorization Server는 처음 받은 `code_challenge`와 비교하여 같은 요청에서 시작된 교환인지 확인한다.
|
||||
|
||||
이때 `S256` 방식을 사용한다. `plain` 방식은 `code_verifier` 자체가 `code_challenge`로 전달되기 때문에 Authorization Request를 관찰한 사람이 그 값을 그대로 알 수 있다. 반면 `S256`은 `code_verifier`를 SHA-256으로 변환한 값을 전달하므로 Authorization Request에 원래의 `code_verifier`가 노출되지 않는다.
|
||||
|
||||
### 3. confidential client에도 PKCE를 함께 쓸 수 있다
|
||||
|
||||
Client 인증을 사용하는 Confidential Client에서도 PKCE는 함께 사용할 수 있다.
|
||||
Client 인증과 PKCE는 보호하는 대상이 다르기 때문이다.
|
||||
Client 인증은 Token Endpoint에 요청한 Client가 올바른 Client인지 확인하고, PKCE는 Authorization Code를 받은 주체가 로그인 시작 시 생성한 `code_verifier`를 가지고 있는지 확인한다. 그래서 두 방식을 같이 사용하면 서로 다른 구간을 각각 보호할 수 있다.
|
||||
|
||||
### 4. public client에서는 implicit flow와 direct access grant를 끈다
|
||||
|
||||
Implicit Flow는 Authorization Code를 거치지 않고 Access Token을 브라우저의 Redirect URI로 직접 전달한다.
|
||||
이 때문에 Token이 브라우저를 통과하고 노출될 수 있는 범위가 넓어진다.
|
||||
|
||||
Direct Access Grant는 애플리케이션이 사용자의 아이디와 비밀번호를 직접 받아 Authorization Server에 전달하는 방식이다.
|
||||
원래 사용자가 IdP에만 제공하면 되는 비밀번호를 애플리케이션도 다루게 된다는 문제가 있다.
|
||||
|
||||
현재 구조에서는 Authorization Code Flow를 사용하고 있으므로 Implicit Flow와 Direct Access Grant는 비활성화했다.
|
||||
|
||||
### 5. Client 종류만으로 브라우저가 Token을 받는지 여부가 결정되지는 않는다.
|
||||
|
||||
Confidential Client가 Authorization Code를 Token으로 교환하더라도, 그 결과로 받은 Access Token을 다시 브라우저에 전달하는 구조를 만들 수 있다.
|
||||
즉, Confidential Client라고 해서 Token이 반드시 서버 내부에만 있는 것은 아니다.
|
||||
|
||||
Client 종류는 `client secret`을 어디에 안전하게 보관할 수 있는지를 나타낸다.
|
||||
반면 Access Token이 브라우저까지 전달되는지는 어느 계층이 실제 API 호출을 담당하도록 설계했는지에 따라 별도로 결정된다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 새 OAuth client를 등록할 때
|
||||
- SPA와 server 중 어디가 code를 교환할지 정할 때
|
||||
- PKCE와 client 인증을 어디에 둘지 정할 때
|
||||
- 기존 client의 종류가 맞는지 다시 볼 때
|
||||
|
||||
## 예외
|
||||
|
||||
- 같은 서비스가 브라우저용 public client와 server용 confidential client를 따로 등록할 수 있다.
|
||||
- backend가 사용자 없이 자기 자격으로 부르는 흐름은 Client Credentials를 쓰는 별도 client다.
|
||||
|
||||
## 예시
|
||||
|
||||
- SPA용 client : public, standard flow만 켜고 implicit flow와 direct grant는 끈다
|
||||
- Mediator용 client : confidential, client_secret_basic으로 token endpoint에서 인증한다
|
||||
- BFF용 client : confidential, PKCE S256을 함께 쓴다
|
||||
- Proxy용 client : confidential, oauth2-proxy가 secret과 verifier로 code를 교환한다
|
||||
- confidential client인 Mediator를 써도 access token은 브라우저 응답에 반환될 수 있다
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
---
|
||||
id: 66c18e42-116c-459f-86bd-b7e4bf394866
|
||||
kind: REFERENCE
|
||||
slug: oauth-token-application-session-boundary
|
||||
title: OAuth Token과 Application Session을 구분하는 기준
|
||||
topic: OAuth/OIDC 인증 경계
|
||||
project: KeyCloak Patterns
|
||||
status: 게시 중
|
||||
version: 33
|
||||
verifiedOn: 2026-08-30
|
||||
studio: "https://hyeonworks.com/studio/documents/66c18e42-116c-459f-86bd-b7e4bf394866/edit"
|
||||
public: "https://hyeonworks.com/references/oauth-token-application-session-boundary"
|
||||
---
|
||||
|
||||
# OAuth Token과 Application Session을 구분하는 기준
|
||||
|
||||
인증 과정에서 만들어지는 상태를 모두 하나의 `로그인 상태`로 보면 안 된다.
|
||||
IdP의 SSO Session, Access Token, Refresh Token, 애플리케이션의 Session Cookie, 인증 Proxy의 Session Cookie는
|
||||
각각 생성하는 주체와 사용하는 주체가 다르고 유효 시간도 서로 다르다.
|
||||
|
||||
예를 들어 Access Token이 만료되었다고 해서 애플리케이션 Session이나 IdP의 SSO Session까지 같이 만료된 건 아니다.
|
||||
반대로 애플리케이션 Session을 삭제했다고 해서 IdP의 SSO Session이나 이미 발급된 Token까지 사라지는 것도 아니다.
|
||||
|
||||
그래서 어떤 Session이나 Token이 남아 있는지, 무엇이 만료되었는지, Logout할 때 어떤 상태를 삭제하거나 무효화해야 하는지를 각각 구분해서 확인한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
||||
JavaScript memory의 OAuth token과 Keycloak SSO session을 구분한 Case다.
|
||||
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
||||
같은 요청 안에서 session cookie와 access token이 함께 움직인다.
|
||||
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
|
||||
BFF에서는 session cookie, JavaScript가 읽는 CSRF token, server-side OAuth token을 각각 다른 용도로 사용한다.
|
||||
- **Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유**
|
||||
Forward-Auth에서는 upstream이 JWT를 직접 검증하지 않고 proxy session을 기반으로 edge가 만든 identity header를 사용한다.
|
||||
|
||||
## 목적
|
||||
|
||||
SPA, Mediator, BFF에서는 Resource Server가 Access Token을 검증한 뒤 JWT의 Claim에서 사용자 정보를 얻을 수 있다. 반면 Forward-Auth 구조에서는 애플리케이션이 인증 Proxy가 전달한 사용자 정보 Header를 사용한다.
|
||||
최종적으로 같은 사용자 이름이 나오더라도, 한쪽은 Access Token을 검증해서 얻은 값이고 다른 한쪽은 신뢰할 수 있는 Proxy가 전달한 값이다.
|
||||
|
||||
Logout과 만료 처리에서도 같은 구분이 필요하다. IdP의 SSO Session, Access Token과 Refresh Token, Application Session은 서로 다른 주체가 관리하고 수명도 다르다. 따라서 Logout할 때 무엇을 삭제하거나 무효화할지, 특정 Credential이 만료되었을 때 어떤 상태를 계속 사용할 수 있는지를 각각 구분해서 설계한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 인증 상태를 종류별로 구분해서 기록한다.
|
||||
|
||||
IdP SSO Session, OAuth Access Token, OAuth Refresh Token, Application Session Cookie, Proxy Session Cookie는 각각 생성하는 주체와 사용하는 목적이 다른 별개의 상태다.
|
||||
|
||||
로그와 진단 정보에서도 어떤 상태를 확인한 것인지 구체적으로 기록한다.
|
||||
예를 들어 단순히 `로그인 상태가 만료되었다`고 남기는 대신 `Application Session이 만료되었다`, `Access Token이 만료되었다`, `Proxy Session이 존재하지 않는다`처럼 실제 Session이나 Token의 종류를 명시한다.
|
||||
|
||||
이렇게 이름을 구분해야 장애를 분석하거나 Logout과 만료 동작을 확인할 때 어떤 상태가 남아 있고 어떤 상태를 삭제하거나 갱신해야 하는지 정확하게 판단할 수 있다.
|
||||
|
||||
### 2. 만든 주체와 주된 소비자로 구분한다
|
||||
|
||||
Access Token, Application Session Cookie, Proxy Session Cookie는 각각 발급하는 주체와 사용하는 주체가 다르다.
|
||||
|
||||
Access Token은 IdP가 발급하고 Resource Server가 요청을 처리할 때 검증한다.
|
||||
Application Session Cookie는 애플리케이션이 발급하고, 이후 브라우저가 보낸 Cookie를 이용해 애플리케이션이 자신의 로그인 Session을 찾는 데 사용한다.
|
||||
Proxy Session Cookie는 인증 Proxy가 발급하고, 이후 Proxy가 인증 상태를 확인할 때 사용한다.
|
||||
|
||||
이처럼 어떤 주체가 Credential을 발급했고, 요청을 처리할 때 어떤 주체가 이를 검증하는지가 다르다면 서로 다른 Credential과 인증 상태로 구분해서 다뤄야 한다.
|
||||
|
||||
### 3. 같은 사용자라도 Credential은 서로 다른 상태를 나타낸다
|
||||
|
||||
Application Session Cookie는 서버에 저장된 Session을 찾기 위한 `session ID`를 브라우저에 전달하는 데 사용한다.
|
||||
실제 Access Token과 Refresh Token은 Cookie 안에 들어 있는 것이 아니라 Authorized Client와 같은 별도의 서버 저장소에 보관된다. 따라서 Session Cookie와 OAuth Token 저장소는 서로 구분해서 봐야 한다.
|
||||
|
||||
Proxy Session Cookie는 반드시 같은 방식으로 동작하는 것은 아니다.
|
||||
별도의 서버 Session Store를 두지 않고, 인증 상태를 확인하는 데 필요한 정보를 Cookie 자체에 담은 뒤 Proxy가 Cookie의 유효성을 검증하는 방식으로 구성할 수도 있다.
|
||||
이 경우 Cookie는 서버에 저장된 Session을 조회하기 위한 `session ID`와는 역할이 다르다.
|
||||
|
||||
### 4. 브라우저에 없는 것을 범위까지 적는다
|
||||
|
||||
브라우저 JavaScript에 OAuth Token을 전달하지 않는 구조에서도 브라우저에 인증과 관련된 상태는 남아 있을 수 있다.
|
||||
예를 들어 BFF 구조에서는 애플리케이션의 `HttpOnly` Session Cookie가 유지될 수 있고, IdP에서는 자신의 도메인에 SSO Session Cookie를 유지할 수 있다.
|
||||
|
||||
따라서 단순히 `브라우저에 인증 정보가 없다`거나 `브라우저에 Credential이 없다`고 표현하면 안 된다.
|
||||
`브라우저 JavaScript에 Access Token과 Refresh Token을 노출하지 않는다`처럼 무엇이 없고 어느 범위에서 접근할 수 없는지를 적는다.
|
||||
|
||||
### 5. 영구 저장과 메모리 보관을 구분한다.
|
||||
|
||||
OAuth Token을 JavaScript Memory에만 보관하면 Local Storage나 Session Storage와 같은 Web Storage에 Token을 지속적으로 저장하지 않을 수 있다.
|
||||
다만 실행 중인 브라우저 JavaScript에서도 Token에 접근할 수 없다는 뜻은 아니다.
|
||||
|
||||
애플리케이션이 Token Endpoint의 응답을 JavaScript로 받아 처리한다면, 실행 중에는 Token 값이 JavaScript가 다루는 메모리에 존재한다. 같은 Origin에서 악성 Script가 실행될 수 있는 상황에서는 Token 응답이나 애플리케이션이 Token을 처리하는 경로가 공격 대상이 될 수 있기 때문에, Web Storage에 Token이 저장되지 않을 뿐이고 XSS를 통해 Token에 접근할 여지는 남는다.
|
||||
|
||||
### 6. 로그아웃 범위를 상태별로 적는다
|
||||
|
||||
애플리케이션에서 Logout하는 것과 IdP의 SSO Session을 종료하는 것은 서로 다른 동작이다.
|
||||
애플리케이션 Session이나 Cookie를 삭제하더라도 IdP의 SSO Session은 그대로 남아 있을 수 있으며, 반대로 IdP Session을 종료하더라도 이미 발급된 Access Token의 처리 방식은 별도로 확인해야 한다.
|
||||
|
||||
특히 Resource Server가 Self-contained JWT Access Token을 매 요청마다 IdP에 확인하지 않고 자체적으로 검증하는 구조에서는 이미 발급된 Token이 Logout과 동시에 자동으로 무효화되지는 않는다.
|
||||
Resource Server는 JWT의 서명과 만료 시간 등 필요한 Claim을 검증하고 Token이 아직 유효하면 요청을 받아들일 수 있다.
|
||||
|
||||
따라서 Denylist처럼 이미 발급된 Token의 상태를 추가로 확인하는 방법을 사용하지 않는다면, 애플리케이션 Logout만으로 기존 Access Token을 즉시 사용할 수 없게 만들 수는 없다.
|
||||
이런 구조에서는 Access Token의 TTL을 짧게 설정해 Logout 이후에도 기존 Token을 사용할 수 있는 시간을 제한하고,
|
||||
Refresh Token과 Session은 각각의 저장 위치와 관리 주체에 맞게 별도로 종료하거나 제거한다.
|
||||
|
||||
### 7. Logout 대상 credential을 구체적으로 적는다
|
||||
|
||||
SPA에서 JavaScript Memory에 보관하던 OAuth Token을 제거하더라도 Keycloak의 SSO Session까지 종료되는 것은 아니다. Keycloak의 SSO Session이 아직 유효하다면 이후 새로운 Authorization Request를 보냈을 때 사용자가 다시 아이디와 비밀번호를 입력하지 않고 인증 절차가 진행될 수 있다.
|
||||
|
||||
따라서 Logout을 단순히 브라우저의 Token이나 Cookie를 삭제하는 동작으로만 정의하면 안 된다.
|
||||
어떤 수준까지 로그아웃할 것인지에 따라 애플리케이션 상태와 IdP의 SSO Session을 각각 어떻게 종료할지 정해야 한다.
|
||||
|
||||
Mediator나 BFF처럼 서버에서 Application Session과 Authorized Client를 함께 관리하는 구조에서는 두 상태의 정리 방법도 각각 명시한다.
|
||||
Application Session을 무효화하는 것과 Authorized Client에 저장된 Access Token 및 Refresh Token을 제거하는 것은 서로 다른 처리기 때문에, Logout 시 어떤 상태를 삭제하고 어떤 상태를 유지할지를 별도로 확인한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 인증 상태를 표나 문서로 정리할 때
|
||||
- 로그아웃과 만료 동작을 설계할 때
|
||||
- 브라우저가 어떤 credential을 저장하거나 전송하는지 설명할 때
|
||||
- 여러 구조를 비교할 때
|
||||
|
||||
## 예외
|
||||
|
||||
- 하나의 요청 흐름 안에서 어떤 Session이나 Token을 의미하는지가 이미 명확한 경우에는 짧은 이름을 사용할 수 있다.
|
||||
다만 문서에서 처음 등장할 때는 전체 이름을 먼저 적어 어떤 상태를 의미하는지 명확하게 정의한다.
|
||||
이후 같은 문맥에서는 의미가 달라지지 않는 범위에서 `Session`, `Access Token`, `Refresh Token`처럼 줄여서 표현할 수 있다.
|
||||
- IdP를 쓰지 않고 애플리케이션이 자체 로그인만 하는 구조에는 SSO session과 access token, refresh token이 없다.
|
||||
|
||||
## 예시
|
||||
|
||||
- IdP SSO session : IdP 도메인의 cookie이고 애플리케이션 memory와 별개다
|
||||
- access token : IdP가 만들고 Resource Server가 서명과 issuer, audience를 검증한다
|
||||
- refresh token : 새 access token을 받는 장기 credential이다
|
||||
- 애플리케이션 session cookie : server-side 로그인 상태를 찾는 credential이다.
|
||||
- proxy session cookie : proxy의 auth endpoint에 제시하는 최소 상태다
|
||||
- CSRF token : cookie가 자동으로 붙는 상태 변경 요청의 의도를 확인한다
|
||||
- identity header : edge가 확인한 사용자 정보이고 JWT token이 아니다
|
||||
Reference in New Issue
Block a user