124 lines
9.5 KiB
Markdown
124 lines
9.5 KiB
Markdown
---
|
|
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 값이다
|