Files
document-haness/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-bff-auth-design.md
T
DongHyeonkaandClaude Fable 5.1 4d50bb939a docs(keycloak): adopt the decomposition contract, fix the redirect URI, strip evaluative prose
- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중
  17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로
  tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8
- 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html
  (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다
- 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference)
- 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다.
  삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

10 KiB

id, kind, slug, title, topic, topicName, project, status, version, verifiedOn, studio, public, sourceRevision, source
id kind slug title topic topicName project status version verifiedOn studio public sourceRevision source
97eddd97-1096-426a-a2c6-a6c5bf1cd09f REFERENCE bff-authentication-design-criteria BFF 인증 구조 설계 기준 oauth-oidc-auth-boundary OAuth/OIDC 인증 경계 KeyCloak Patterns 게시 중 21 2026-08-30 https://hyeonworks.com/studio/documents/97eddd97-1096-426a-a2c6-a6c5bf1cd09f/edit https://hyeonworks.com/references/bff-authentication-design-criteria keycloak-patterns-lab@2026-08
final/document.md#검토한-선택지와-막힌-지점-ap3
final/document.md#선택의-이유와-지킨-경계-ap3
final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3

BFF 인증 구조 설계 기준

BFF(Backend For Frontend)는 화면에 필요한 API를 브라우저 대신 호출해 주는 백엔드다. 이 구조에서는 OAuth 토큰을 서버가 보관하고, 브라우저는 토큰 대신 세션 쿠키로 BFF에 요청한다.

쿠키로 인증하는 요청을 지킬 CSRF 검증, OAuth 토큰을 보관할 인가된 클라이언트(authorized client) 저장소, 로그아웃할 때 세션과 토큰을 함께 지우는 방법, BFF가 호출한 Resource Server에서 오류가 났을 때 이를 브라우저에 무엇으로 바꿔 돌려줄지를 같이 설계해야 한다.

관계

  • Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 이 기준의 항목 중 무엇이 실제로 구현되었고 무엇이 비어 있는지 센 기록이다.
  • 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 이 기준의 저장소 항목에 아직 답을 주지 못한 질문이다.
  • BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가 세션과 토큰을 어느 저장소에 둘지는 이 기준에서 아직 정하지 않았다.
  • BFF가 OAuth Token을 관리하는 조건 그 결정이 PROPOSED인 동안에는 이 문서가 실제 적용 기준이다.

목적

BFF 구조에서는 BFF가 authorization code를 토큰으로 교환하고 그 액세스 토큰으로 Resource Server를 호출하므로, 브라우저의 로그인 상태를 담은 세션과 토큰을 보관하는 인가된 클라이언트를 함께 관리해야 한다.

쿠키가 자격 증명이 되면 브라우저가 요청마다 자동으로 붙여 보내기 때문에, 값을 바꾸는 요청은 사용자가 의도한 것인지 따로 확인해야 한다. 서버를 재시작하거나 요청이 다른 레플리카로 가더라도 로그인 상태를 이어 갈 저장소도 함께 필요하다.

규칙

1. 브라우저에는 OAuth 토큰을 전달하지 않는다

액세스 토큰과 리프레시 토큰은 브라우저가 아니라 BFF 서버 쪽 저장소인 인가된 클라이언트에 보관한다. 브라우저는 이 토큰을 직접 쓰지 않고 세션 쿠키로 BFF에 요청한다.

BFF는 이 세션을 확인한 뒤, 보관해 둔 액세스 토큰으로 Authorization: Bearer ... 헤더를 새로 만들어 Resource Server를 호출한다. 브라우저가 보낸 세션 쿠키는 Resource Server까지 가지 않는다. 세션 쿠키는 브라우저와 BFF 사이의 자격 증명이고, 액세스 토큰은 BFF와 Resource Server 사이의 자격 증명이다.

2. 쿠키가 자격 증명이면 상태 변경 요청에 CSRF 검증을 둔다

BFF 구조에서는 브라우저가 요청할 때 세션 쿠키를 자동으로 보내기 때문에, 데이터를 만들고 고치고 지우는 것처럼 서버의 상태를 바꾸는 요청에는 그 요청이 실제 사용자의 의도에서 나왔는지 확인하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 필요하다.

현재 구성에서는 서버가 CSRF 토큰을 쿠키로 내려보내고, JavaScript가 그 값을 읽어 요청 헤더에 다시 담아 보낸다. 서버는 쿠키와 헤더를 함께 확인해 요청을 검증한다.

응답 본문에 실린 값이 별도의 처리를 거친 값이라면, 클라이언트는 CSRF 쿠키에서 값을 읽어 헤더에 넣어야 한다. 잘못된 값을 보내면 정상적인 요청이라도 CSRF 검증에 실패해 403 Forbidden 응답을 받는다.

SameSite와 CSRF 토큰도 서로 하는 일이 다르다. SameSite는 다른 사이트에서 시작한 요청에 브라우저가 쿠키를 실어 보낼지 제한하는 정책이고, CSRF 토큰은 쿠키가 붙어 들어온 상태 변경 요청이 정상적인 클라이언트에서 만들어졌는지 서버가 확인하는 값이다. 또 SameSite는 오리진이 아니라 사이트를 기준으로 판단하므로, 포트가 달라 오리진은 다르지만 같은 사이트에 속하는 요청도 있을 수 있다.

3. 세션과 인가된 클라이언트의 수명주기를 따로 설계한다

애플리케이션 세션과 인가된 클라이언트는 저장하고 조회하는 값이 서로 다르다. 세션은 세션 ID로 조회하지만, 인가된 클라이언트는 client registration 이름과 principal name으로 조회한다. 그래서 여러 인스턴스가 상태를 나눠 쓰도록 공유 저장소를 들일 때도 세션 저장소와 인가된 클라이언트 저장소를 각각 어떻게 구성할지 확인해야 한다.

특히 인가된 클라이언트의 조회 기준에는 세션 ID가 들어가지 않는다. 그래서 같은 사용자가 두 브라우저에서 같은 클라이언트로 로그인하면 두 세션이 같은 인가된 클라이언트를 함께 쓰거나, 나중에 로그인하면서 저장된 토큰 정보가 갱신될 수 있다. 브라우저나 세션마다 서로 다른 토큰을 유지해야 한다면 세션 ID까지 조회 기준에 넣어 토큰을 구분하는 저장 구조를 따로 설계해야 한다.

운영 환경에서는 서버가 재시작되거나 요청이 다른 레플리카로 가더라도 로그인 상태와 토큰을 계속 쓸 수 있는지도 따져야 한다. 세션과 인가된 클라이언트를 공유 저장소에 둘지, 요청을 같은 인스턴스로만 보내는 세션 어피니티를 쓸지 등을 결정해야 한다. 토큰을 외부 저장소에 보관한다면 액세스 토큰과 리프레시 토큰을 어떻게 보호할지도 정해야 하며, 저장할 때 암호화한다면 암호화 키를 어디에 두고 어떻게 교체할지까지 함께 설계해야 한다.

로그아웃에서도 두 상태를 각각 정리해야 한다. 세션과 인가된 클라이언트는 조회 기준도 저장소도 다르므로, 로그아웃할 때 둘이 모두 지워지는지 각각 확인해야 한다.

4. Downstream 오류를 클라이언트 응답으로 변환한다

BFF가 Resource Server의 오류를 그대로 브라우저에 넘기면 화면에서는 오류의 원인을 일관되게 판단하기 어렵다. Resource Server가 401 Unauthorized를 돌려줬다면 액세스 토큰이 만료되었거나 더 이상 유효하지 않은 상황인지 확인하고, 필요하면 토큰 갱신이나 재로그인으로 이어야 한다. 하지만 인증은 정상적으로 되었는데 해당 기능을 쓸 권한이 없어 403 Forbidden이 나온 경우에는 권한 부족으로 처리해야 한다.

Resource Server가 응답하지 않거나 처리가 늦어지는 경우에도 별도의 규칙이 필요하다. 요청을 얼마나 기다릴지 타임아웃을 정하고, 다시 보내도 되는 요청에는 재시도 정책을 적용한다. 장애가 반복되는 Resource Server에 계속 요청을 보내지 않도록 서킷 브레이커를 둘지도 함께 결정한다.

화면의 모든 요청이 BFF를 지나는 구조라면 이런 오류 처리 규칙도 BFF에서 일관되게 적용하는 편이 낫다. 그렇지 않으면 같은 종류의 오류를 화면마다 서로 다르게 판단하고 처리할 수 있다.

이 규칙이 정하는 것은 Resource Server가 돌려준 오류를 무엇으로 바꿔 내보낼지까지다. 토큰 갱신이 실패했을 때와 로그아웃한 뒤에 브라우저가 무엇을 받는지도 같은 종류의 질문이지만, 그것을 어느 계층이 최종 HTTP 응답으로 바꿀지는 이 기준에서 정하지 않았다.

5. BFF에서도 XSS 방어는 별도로 필요하다

BFF 구조에서는 액세스 토큰과 리프레시 토큰을 서버가 보관하므로 브라우저의 JavaScript가 OAuth 토큰 원문에 직접 접근하지 않게 할 수 있다. 하지만 이것이 브라우저에서 실행되는 악성 JavaScript까지 막아 주지는 않는다.

같은 오리진에서 악성 스크립트가 실행되면 사용자의 세션으로 BFF 엔드포인트를 호출할 수 있다. 지금처럼 JavaScript가 CSRF 쿠키를 읽어 헤더에 넣는 구조라면 악성 스크립트도 같은 방법으로 CSRF 토큰을 읽어 요청을 만들 수 있다.

BFF에서는 OAuth 토큰 원문이 브라우저 JavaScript에 직접 드러나지 않지만, XSS(Cross-Site Scripting) 자체를 막는 CSP(Content Security Policy)와 출력 인코딩 같은 보호 조치, 그리고 외부 스크립트와 의존성을 안전하게 관리하는 방법은 따로 적용해야 한다. 악성 스크립트가 사용자의 세션으로 BFF를 호출하더라도 허용된 작업만 할 수 있도록, 애플리케이션의 인가 역시 요청마다 검증해야 한다.

적용 조건

  • 브라우저가 OAuth 토큰을 받아서는 안 될 때
  • 백엔드가 화면에 맞춰 여러 API를 조합해야 할 때
  • 로그인 상태를 애플리케이션이 소유해야 할 때
  • 호출할 API가 늘어나도 브라우저는 BFF 하나만 알게 하고 싶을 때

예외

  • 서버에 상태를 두지 않는 직접 API 호출과 독립 클라이언트가 핵심이면 BFF 구조로 설계하지 않는다. 서버 상태와 단일 장애 지점만 늘어난다.
  • 브라우저가 API를 직접 호출해야 하는 구조라면 갱신용 자격 증명만 서버로 분리하는 구조가 맞다.
  • 서버에 상태를 둘 수 없는 환경이면 브라우저가 토큰을 직접 다루는 구조가 더 단순하다.

예시

  • 브라우저 요청에는 Authorization 헤더가 없고 세션 쿠키만 있다
  • BFF가 인가된 클라이언트에서 액세스 토큰을 읽어 Resource Server용 Bearer 요청을 새로 만든다
  • CSRF 헤더가 없는 POST는 403이 되고, 쿠키에 든 원래 값을 헤더에 넣은 POST는 200이 된다
  • 응답 본문의 토큰은 가려 놓은 값이고, 헤더에 넣는 값은 쿠키에 든 원래 값이다