소비하는 FastAPI 앱에 한 줄(install_bff)로 OIDC 로그인 / 세션 / CSRF / 토큰 자동 갱신 / 인증 리버스 프록시를 붙입니다.
요구사항: Python ≥ 3.10, FastAPI ≥ 0.115, Authlib ≥ 1.3.2
목차
주요 기능
- BFF 패턴 — 브라우저는 불투명한
sid쿠키만 보유하고, 토큰(access/refresh/id)·userinfo는 서버사이드 세션 스토어에 저장 → XSS로부터 토큰 보호 - 멀티 프로바이더 — Keycloak · Okta · Google · Azure AD(Entra) · GitHub 빌트인 + 표준 OIDC IdP(예: Dream Security)를 코드 수정 없이
OIDC_BFF_PROVIDERS로 추가 - 세션 스토어 — 인메모리(단일 인스턴스) / Redis(멀티 인스턴스·클러스터, per-sid 락 + 슬라이딩 TTL)
- silent refresh — 액세스 토큰 만료 임박 시 자동 갱신(동시 요청 single-flight)
- 세션 보안 — 세션 바인딩 CSRF(double-submit) · 슬라이딩 세션 + 절대 수명 상한 · sid/CSRF 쿠키 lockstep
- BFF 리버스 프록시 —
/api/*를 다운스트림 API로 포워딩하며 access token을 Bearer로 주입(쿠키/CSRF/hop-by-hop 헤더 차단) - 순수 config 주입 — 라이브러리는
.env를 몰래 읽지 않음. 명시적BFFConfig(...)또는 opt-inBFFConfig.from_env()
설치
# uv
uv add axmp-oidc-bff-provider
# pip
pip install axmp-oidc-bff-provider
세션 스토어로 Redis를 쓰려면 Redis 서버가 필요합니다(아래 세션 백엔드 참고).
빠른 시작 (install_bff)
from fastapi import FastAPI
from axmp_oidc_bff_provider import install_bff, BFFConfig
config = BFFConfig.from_env() # 환경변수/.env 에서 로드 (opt-in)
app = FastAPI()
install_bff(app, config)
install_bff(app, config) 한 번이 다음을 모두 와이어링합니다:
- 미들웨어:
SessionMiddleware+CSRFMiddleware(항상 짝) +CORSMiddleware(opt-in:cors_enabled이고frontend_origins가 있을 때) - 라우터:
/oauth2/*(로그인·콜백·로그아웃·userinfo),/api/*프록시(upstream_api_url설정 시에만) - lifespan: 세션 스토어 + 공유 httpx 클라이언트 생성/정리 (앱의 기존 lifespan을 덮지 않고 감싸서 합성)
같은 앱에 두 번 호출하면 RuntimeError(fail-fast)로 막습니다.
내 라우트 보호하기
from fastapi import Depends
from axmp_oidc_bff_provider import get_current_user, get_current_user_optional
@app.get("/me")
def me(user: dict = Depends(get_current_user)): # 미인증 시 401
return user
@app.get("/")
def index(user=Depends(get_current_user_optional)): # 미인증 시 None
return {"authenticated": user is not None}
설정 (BFFConfig)
BFFConfig는 순수 pydantic 모델입니다. 명시적 생성은 환경변수를 읽지 않습니다:
# 명시적 — env 무시
config = BFFConfig(
default_provider="keycloak",
keycloak_base_url="https://kc.example.com",
keycloak_realm="myrealm",
keycloak_client_id="my-app",
keycloak_client_secret="...",
session_secret_key="<랜덤 시크릿>",
session_backend="redis",
redis_url="redis://:password@redis:6379/0",
frontend_origins=["https://app.example.com"],
)
# 또는 env/.env 에서 (opt-in)
config = BFFConfig.from_env()
주요 필드 — 모든 env 변수는 OIDC_BFF_ 프리픽스를 씁니다(소비 앱의 다른 설정과 충돌 방지). 즉 env 변수명 = OIDC_BFF_ + 필드명 대문자 (예: default_provider → OIDC_BFF_DEFAULT_PROVIDER, keycloak_base_url → OIDC_BFF_KEYCLOAK_BASE_URL):
| 필드 / env | 기본값 | 설명 |
|---|---|---|
default_provider |
keycloak |
/oauth2/start에 ?provider= 없을 때 기본 IdP |
providers |
{} |
커스텀 OIDC 프로바이더 (아래 참고). env는 JSON |
verify_ssl |
true |
모든 OIDC HTTP 호출의 TLS 인증서 검증. 로컬 자체서명 IdP에서만 false |
app_base_url |
http://localhost:8000 |
이 백엔드의 공개 URL — redirect_uri 조립에 사용 |
frontend_url |
http://localhost:5173 |
로그인/로그아웃 후 브라우저를 보낼 SPA 주소 |
frontend_origins |
["http://localhost:5173"] |
CORS 허용 오리진 + 오픈리다이렉트 허용목록 |
cors_enabled |
true |
CORS 미들웨어 추가 여부(opt-out 가능) |
csrf_exempt_paths |
[] |
소비 앱이 CSRF 검증에서 추가로 제외할 경로 (아래 참고). env는 JSON 배열 |
csrf_safe_methods |
[] |
소비 앱이 CSRF-safe로 추가할 HTTP 메서드 (아래 참고). env는 JSON 배열 |
session_secret_key |
(insecure 기본값) | 세션 쿠키 서명 키. 프로덕션 필수 변경 |
session_cookie_name |
bff_session |
서명 세션 쿠키 이름 |
session_https_only |
false |
쿠키 Secure 플래그 (HTTPS 환경에서 true) |
session_same_site |
lax |
쿠키 SameSite |
cookie_domain |
"" |
쿠키 Domain (분리 호스트 배포용; 비우면 host-only) |
session_ttl_seconds |
28800 (8h) |
서버사이드 세션 수명 |
session_sliding |
true |
인증 요청마다 TTL 연장(절반 경과 시에만 기록) |
session_absolute_max_seconds |
604800 (7d) |
슬라이딩과 무관한 절대 수명 상한(0=무제한) |
session_backend |
redis |
memory 또는 redis |
redis_url |
redis://localhost:6379/0 |
Redis 접속 URL (인증 시 redis://:pw@host:6379/0) |
redis_key_prefix |
bff:sess: |
Redis 키 프리픽스 |
refresh_leeway_seconds |
30 |
액세스 토큰 만료 N초 전 미리 갱신 |
refresh_http_timeout_seconds |
8.0 |
refresh HTTP 타임아웃(per-sid 락 TTL 미만이어야 함) |
upstream_api_url |
"" |
BFF 프록시 대상. 비우면 /api 프록시 비활성 |
생성 시 session_https_only=True(또는 app_base_url이 https)인데 시크릿이 insecure 기본값이면 검증 에러로 fail-fast 합니다.
세션 백엔드
session_backend로 두 가지 서버사이드 스토어 중 하나를 선택합니다.
memory— 프로세스 로컬 인메모리. 단일 인스턴스·로컬 개발용(재시작 시 소실, 노드 간 공유 안 됨).redis(기본) — Redis 백엔드. 멀티 인스턴스·클러스터에서 세션을 공유하고 재시작에도 유지됩니다. per-sid 락으로 silent refresh를 single-flight 하고 슬라이딩 TTL을 지원합니다.
Redis 인증이 필요하면 redis_url에 자격증명을 포함하세요(예: redis://:password@host:6379/0).
CSRF 예외 경로 추가
CSRFMiddleware는 unsafe 메서드(POST/PUT/PATCH/DELETE)에 X-CSRF-Token 헤더를 요구합니다. 라이브러리는 OAuth 리다이렉트 플로우와 헬스 프로브를 빌트인 예외로 갖고 있습니다(/oauth2/start, /oauth2/callback, /oauth2/sign_out, /healthz).
소비 앱이 자기 경로를 추가로 제외하려면 csrf_exempt_paths에 넣으면 됩니다. 빌트인 예외는 항상 유지되고 여기에 더해집니다(덮어쓰지 않음).
config = BFFConfig(
csrf_exempt_paths=[
"/webhooks/stripe", # 정확히 이 경로만
"/internal/*", # "/internal/" 로 시작하는 서브트리 전체
"/users/{user_id}/children", # 경로 중간의 path parameter
],
)
install_bff(app, config)
env로 주입할 때는 JSON 배열입니다:
OIDC_BFF_CSRF_EXEMPT_PATHS=["/webhooks/stripe","/internal/*","/users/{user_id}/children"]
매칭 규칙 — 대상은 요청 경로 전체(ASGI scope["path"], 즉 Starlette 라우터가 매칭하는 것과 완전히 동일한 문자열):
| 패턴 | 매칭 | 비매칭 |
|---|---|---|
/webhooks/stripe |
/webhooks/stripe (정확히 일치) |
/webhooks/stripe/extra, /webhooks/github |
/internal/* |
/internal/, /internal/tasks/run (접두어 /internal/) |
/internal, /internal-tools |
/users/{user_id}/children |
/users/7/children, /users/a-b/children |
/users/children, /users/7/8/children, /users/7/children/extra, /users/7/parents |
path parameter ({name}) — FastAPI 라우트처럼 경로 중간에 가변 세그먼트가 있을 때 씁니다.
{name}은 세그먼트 전체여야 하고, 비어 있지 않은 한 세그먼트에만 매칭됩니다(/를 넘지 않음). 이름 자체는 매칭에 쓰이지 않으니 라우트의 파라미터명과 같게 적어두면 읽기 좋습니다.- 여러 번 쓸 수 있고(
/orgs/{org_id}/users/{user_id}), 후행/*와 조합할 수 있습니다(/users/{user_id}/children/*→/users/7/children/이하 전체). - 리터럴 세그먼트가 최소 하나는 있어야 합니다.
/{tenant}나/{tenant}/*는 사실상 catch-all 이라/*와 같이 거부됩니다(/{tenant}/webhooks/receive는 OK). - 세그먼트 일부만 파라미터인 형태(
/users/u{id})나 빈/중첩 중괄호({},{a{b}})는 config 로드 시 거부됩니다.
앱이 프리픽스 아래에 서비스되면(root_path="/bff", 서브앱 마운트 등) 패턴에도 그 프리픽스를 포함해야 합니다(/bff/webhooks/*) — 매칭 대상이 프리픽스가 붙은 요청 경로 전체이기 때문입니다. 빠뜨리면 계속 403이 나는(=fail-closed) 형태로 드러납니다.
잘못된 설정은 config 로드 시점에 fail-fast 합니다:
/로 시작하지 않는 값 (webhooks/stripe)- 맨 끝의
/*이외의*(글롭 미지원)./internal*처럼 슬래시 없는 형태도 거부합니다 — 의도치 않게 형제 경로(/internal-admin/delete)까지 덮기 때문입니다 - 전체를 제외해 CSRF 보호를 무력화하는
/* - 잘못된
{name}파라미터: 세그먼트 일부만 파라미터(/users/u{id}), 빈/중첩/짝 안 맞는 중괄호, 그리고 리터럴 세그먼트가 하나도 없는 패턴(/{tenant}/*)
검증을 우회해 설정하는 경우(model_copy(update=...), model_construct, 속성 직접 할당)에도 매처가 fail-closed 합니다: 모든 경로에 매칭될 패턴(/*, *, /{tenant}/*)은 경고 로그와 함께 무시되고, 잘못된 형태의 {name} 세그먼트는 리터럴로 취급되어 어떤 경로에도 매칭되지 않습니다.
⚠️ 예외 경로는 다른 오리진에서 사용자 쿠키를 붙여 호출할 수 있습니다. 세션을 대신해 상태를 변경하지 않는 엔드포인트, 또는 자체 인증(서명 헤더·mTLS·API 키)을 갖춘 엔드포인트만 제외하세요.
upstream_api_url이 설정된 상태에서 BFF 프록시 표면(/api/*)을 제외하면 경고 로그가 남습니다 — 프록시는 세션의 access token을 붙여 포워딩하므로, 제외 시 어떤 오리진이든 쿠키만으로 인증된 상태 변경을 유발할 수 있습니다(치명적이지 않은 경고이며, 업스트림이 자체 인증을 갖춘 경우를 위해 허용은 유지).예외 경로에서는 미들웨어가 세션 번들을 조회하지 않습니다(빌트인 예외와 동일). 따라서 인증 의존성(
get_current_user등)을 쓰지 않는 예외 경로 요청에서는 슬라이딩csrf_token쿠키 재발급이 일어나지 않습니다.
CSRF safe 메서드 추가
CSRF 검증은 safe 메서드를 건너뜁니다. 라이브러리 빌트인은 상태를 바꾸지 않는 네 가지(GET, HEAD, OPTIONS, TRACE)입니다.
앱이 이 밖의 read-only 메서드를 쓴다면 — 예: 본문을 갖는 조회용 HTTP QUERY, WebDAV 계열의 PROPFIND/REPORT/SEARCH — csrf_safe_methods에 추가하면 됩니다. 빌트인은 항상 유지되고 여기에 더해집니다(덮어쓰지 않음).
config = BFFConfig(
csrf_safe_methods=["QUERY"], # GET/HEAD/OPTIONS/TRACE 는 그대로 유지
)
install_bff(app, config)
env로 주입할 때는 JSON 배열입니다:
OIDC_BFF_CSRF_SAFE_METHODS=["QUERY"]
- 설정값은 공백 제거 + 대문자로 정규화됩니다(
" query "→QUERY). - 반면 들어온 요청의 메서드는 그대로 비교합니다. HTTP 메서드는 대소문자를 구분하므로 소문자
query요청은QUERY가 아니며, 기존과 동일하게 unsafe로 처리됩니다. csrf_exempt_paths가 경로 단위 예외라면, 이쪽은 메서드 단위(모든 경로에 적용) 예외입니다.
거부되는 값은 오타뿐입니다. HTTP 메서드 토큰이 아닌 값("GET POST", "GE/T" — 한 항목에 하나의 메서드만)은 어떤 request.method와도 같아질 수 없으므로 config 로드 시 fail-fast 합니다.
어떤 메서드에 CSRF 보호가 필요한지는 소비 앱의 정책이므로, 위험한 값도 거부하지 않고 경고 로그와 함께 허용합니다.
config = BFFConfig(csrf_safe_methods=["QUERY", "POST"]) # 허용됨 (경고 로그)
OIDC_BFF_CSRF_SAFE_METHODS=["QUERY","POST"]
- 상태 변경 메서드 —
POST/PUT/PATCH/DELETE및 WebDAV 계열 쓰기 메서드(PROPPATCH,MKCOL,COPY,MOVE,LOCK/UNLOCK,ACL, RFC 3253 버저닝,PURGE등) — 는 강한 경고를 남깁니다. 해당 메서드의 CSRF 검사가 앱 전역에서 사라진다는 뜻이기 때문입니다. 자체 인증(서명 헤더·mTLS·API 키)이 있는 경우에만 사용하고, 특정 엔드포인트만 열고 싶다면 경로 단위csrf_exempt_paths를 쓰세요. - 라이브러리가 read-only로 아는 메서드(
QUERY,PROPFIND,REPORT,SEARCH)가 아닌 커스텀 메서드는 약한 경고를 남깁니다 — 쓰기 여부를 라이브러리가 알 수 없기 때문입니다.
경고는 실제로 적용되는 매처 단계에서도 한 번 더 남습니다(메서드 집합당 1회). 검증을 우회해 설정하면(model_copy(update=...), model_construct, 속성 직접 할당) config 로드 경고가 아예 뜨지 않기 때문에, 이쪽이 유일한 경고 지점입니다.
⚠️ safe 메서드는 모든 경로에서 CSRF 검사를 건너뜁니다.
upstream_api_url이 설정돼 있으면 세션 access token을 붙여 포워딩하는/api/*프록시도 포함이므로,POST등을 safe로 두면 세션 쿠키가 전달되는 오리진(SameSite=lax/strict면 same-site 오리진,SameSite=None이면 사실상 모든 오리진)에서 사용자 쿠키만으로 인증된 상태 변경 호출을 유발할 수 있습니다. 단, 프록시는 여전히 세션을 요구하므로(get_current_session) 로그인하지 않은 요청은 401입니다.메서드 오버라이드 미들웨어(
X-HTTP-Method-Override등)를CSRFMiddleware바깥쪽에 설치하지 마세요.install_bff는 CSRF를 가장 안쪽에 넣으므로, 나중에 추가한 오버라이드 미들웨어가 먼저 실행되면POST가 safe 메서드로 바뀐 뒤에야 CSRF가 보게 됩니다.
OIDC 프로바이더
빌트인 (5종)
각 프로바이더 자격증명을 env/BFFConfig로 설정하면 자동 등록됩니다.
모든 env 변수는
OIDC_BFF_프리픽스를 씁니다.
| Provider | 핵심 설정 |
|---|---|
| keycloak | OIDC_BFF_KEYCLOAK_BASE_URL, OIDC_BFF_KEYCLOAK_REALM, OIDC_BFF_KEYCLOAK_CLIENT_ID, OIDC_BFF_KEYCLOAK_CLIENT_SECRET |
| okta | OIDC_BFF_OKTA_ORG_URL, OIDC_BFF_OKTA_CLIENT_ID, OIDC_BFF_OKTA_CLIENT_SECRET |
OIDC_BFF_GOOGLE_CLIENT_ID, OIDC_BFF_GOOGLE_CLIENT_SECRET |
|
| azure | OIDC_BFF_AZURE_TENANT_ID, OIDC_BFF_AZURE_CLIENT_ID, OIDC_BFF_AZURE_CLIENT_SECRET |
| github | OIDC_BFF_GITHUB_CLIENT_ID, OIDC_BFF_GITHUB_CLIENT_SECRET (비-OIDC OAuth2; userinfo는 api.github.com에서 조회) |
Keycloak이 구버전(레거시
/auth컨텍스트 경로)이면OIDC_BFF_KEYCLOAK_BASE_URL에/auth를 포함하세요(예:https://kc.example.com/auth).
커스텀 (표준 OIDC IdP — 코드 수정 불필요)
.well-known/openid-configuration discovery 문서를 노출하는 IdP는 OIDC_BFF_PROVIDERS(JSON)로 추가합니다:
OIDC_BFF_DEFAULT_PROVIDER=dream
OIDC_BFF_PROVIDERS={"dream": {"server_metadata_url": "https://sso.dreamsecurity.example/.well-known/openid-configuration", "client_id": "my-app", "client_secret": "...", "scope": "openid email profile", "client_kwargs": {"code_challenge_method": "S256"}}}
런타임에는 /oauth2/start?provider=dream 으로 선택합니다. 키 이름이 빌트인과 겹치면 커스텀이 우선합니다.
제공 엔드포인트
install_bff / make_auth_router 가 /oauth2 프리픽스로 추가하는 라우트입니다. 경로·쿼리 파라미터는 AI Gateway OAuth2 plugin(oauth2-proxy 계열) 컨벤션에 맞췄습니다:
| Method | Path | 설명 |
|---|---|---|
| GET | /oauth2/start?rd=<url>&provider=<name> |
선택한 IdP로 302 리다이렉트(PKCE). provider 생략 시 기본값. rd(복귀경로)는 frontend_origins 내에서만 허용 |
| GET | /oauth2/callback |
인가 코드 교환 → 서버사이드 세션(sid) 생성 → rd/frontend_url로 리다이렉트. 실패 시 JSON 에러 대신 frontend_url?error=<code>로 리다이렉트(아래 표) |
| GET | /oauth2/sign_out?rd=<url> |
로컬 세션 삭제 + IdP RP-initiated logout. rd(검증됨)를 post_logout_redirect_uri로 사용 |
| GET | /oauth2/userinfo |
현재 사용자 OIDC userinfo 클레임 원본(JSON), 인증 필요 |
IdP/discovery 장애 시 /oauth2/start은 traceback 노출 없이 502로 graceful 처리됩니다.
/oauth2/callback은 브라우저 최상위 내비게이션이므로 JSON 에러를 렌더링하지 않습니다. 로그인을 완료할 수 없으면 frontend_url로 302 리다이렉트하면서 error 쿼리 파라미터를 붙입니다(frontend_url의 기존 쿼리는 보존). SPA는 이 값을 읽어 안내 문구를 띄우고 /oauth2/start로 다시 보내면 됩니다. 실제 원인은 서버 warning 로그에만 남고 클라이언트에는 노출되지 않습니다.
error 값 |
상황 |
|---|---|
login_session_expired |
세션에 진행 중인 로그인이 없음. 세션 쿠키 만료, 또는 쿠키가 콜백에 실리지 않음(SameSite=strict, cookie_domain·app_base_url 불일치 등) |
login_failed |
진행 중인 로그인은 있으나 코드 교환 실패. state 불일치(다른 탭에서 로그인을 새로 시작해 이전 state가 밀려난 경우 포함), IdP 오류/거부, access token 없음, userinfo 조회 실패 |
이미 로그인된 세션(sid)이 있는 상태에서 콜백 URL을 다시 열면(새로고침 등) 에러 없이 frontend_url로 리다이렉트합니다.
BFF 리버스 프록시 (upstream_api_url 설정 시)
| Method | Path | 설명 |
|---|---|---|
| ANY | /api/{path} |
인증된 요청을 upstream_api_url로 포워딩, 세션의 access token을 Authorization: Bearer로 주입. 인입 Cookie·X-CSRF-Token·X-Forwarded-*·hop-by-hop 헤더 제거, 업스트림 Set-Cookie 미전달, 응답 스트리밍 |
SPA가 unsafe 메서드(POST 등)를 호출할 때는 X-CSRF-Token 헤더가 필요합니다(콜백이 세팅한 읽기 가능 csrf_token 쿠키 값을 echo).
고급 사용
대부분의 앱은 install_bff 한 번으로 충분합니다. 아래는 자체 lifespan·미들웨어를 함께 쓰거나, 와이어링을 직접 제어하고 싶을 때를 위한 가이드입니다.
커스텀 lifespan
소비 앱이 자체 lifespan(DB 풀, 백그라운드 작업 등)을 쓰고 싶으면, 평소처럼 FastAPI(lifespan=...)에 넘긴 뒤 install_bff를 호출하면 됩니다. install_bff가 호출 시점에 등록돼 있는 lifespan을 읽어 BFF lifespan으로 바깥에서 감쌉니다(소비자 lifespan은 그 안쪽에 중첩).
from contextlib import asynccontextmanager
from fastapi import FastAPI
from axmp_oidc_bff_provider import install_bff, BFFConfig
config = BFFConfig.from_env()
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup — BFF lifespan이 먼저 진입한 뒤라
# app.state.bff_session_store / bff_http / bff_auth_service 를 바로 쓸 수 있음
app.state.my_db = await create_db_pool(...)
yield
# shutdown
await app.state.my_db.aclose()
app = FastAPI(lifespan=lifespan) # 1) 내 lifespan 을 먼저 등록
install_bff(app, config) # 2) BFF 가 그 위를 감쌈
- 호출 순서가 중요: lifespan 은
FastAPI()생성 시 넘기고,install_bff는 그 뒤에 호출합니다. (install_bff호출 후app.router.lifespan_context를 덮어쓰면 BFF lifespan 이 사라집니다.) - 실행 순서:
BFF setup → 내 startup → (요청 처리) → 내 shutdown → BFF teardown. BFF 가 바깥이라 내 startup 에서app.state.bff_*를 바로 쓸 수 있고, 내 startup 이 실패해도 BFF teardown(세션 스토어·httpx 클라이언트 close)은 정상 수행됩니다.
커스텀 lifespan 이 없으면
install_bff만 호출하면 됩니다 — BFF lifespan 이 단독으로 동작합니다. 미들웨어·라우터까지 직접 조립하는 경우엔 아래 고급 커스텀 설정 에서bff_lifespan(config)를 직접 사용하세요.
미들웨어 추가
install_bff는 내부에서 CSRF → Session → CORS 순으로 미들웨어를 깝니다(결과 스택은 바깥→안으로 CORS → Session → CSRF → routes). 여기에 내 미들웨어를 더할 때는 install_bff 앞/뒤 어디서 add_middleware를 부르냐로 위치가 정해집니다 — Starlette의 add_middleware는 항상 맨 앞에 prepend하므로 나중에 추가한 게 더 바깥입니다.
| 추가 시점 | 위치 | request flow |
|---|---|---|
install_bff 전 |
BFF 레이어 안쪽 (CSRF보다 안, 라우트에 가깝) | CORS → Session → CSRF → 내것 → routes |
install_bff 후 |
BFF 레이어 바깥 (CORS보다 밖) | 내것 → CORS → Session → CSRF → routes |
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
from axmp_oidc_bff_provider import install_bff, BFFConfig
config = BFFConfig.from_env()
app = FastAPI(lifespan=lifespan) # (커스텀 lifespan 을 쓰면)
# (A) request.session 등 세션/인증 컨텍스트가 필요한 미들웨어 → install_bff "전"
# → SessionMiddleware 안쪽에 위치해 request.session 사용 가능
app.add_middleware(MyTenantContextMiddleware)
install_bff(app, config)
# (B) 전체를 감싸는 cross-cutting(요청ID·로깅·타이밍·GZip·보안헤더) → install_bff "후"
app.add_middleware(GZipMiddleware, minimum_size=1024)
app.add_middleware(RequestIDMiddleware) # 나중 호출이 더 바깥
# 결과: RequestID → GZip → CORS → Session → CSRF → MyTenantContext → routes
request.session을 읽는 미들웨어는 반드시install_bff전에 추가하세요. 바깥(후)에 두면 SessionMiddleware가 아직 안 돌아request.session접근이AssertionError로 터집니다.- CSRF보다 안쪽 = CSRF를 통과한 요청만 봅니다(unsafe 메서드 기준). CSRF 거부까지 포함해 로깅하려면
install_bff후(바깥)에 두세요. - CORS는 가장 바깥이 정석(프리플라이트 우선 응답).
install_bff후에 무거운/조기 응답 미들웨어를 두면 CORS 밖이 되니 유의하세요(로깅·요청ID 정도는 무방). - 둘 다 앱 시작 전에 호출해야 합니다 — 시작 후나 lifespan 안에서
add_middleware를 부르면RuntimeError(Cannot add middleware after an application has started)가 납니다. - 생성자
FastAPI(lifespan=..., middleware=[Middleware(X, ...)])로 넘긴 것도 "install_bff 전 추가"와 동일하게 innermost가 됩니다.
순서를 상대적 위치에 의존하지 않고 완전히 명시적으로 짜려면 아래 고급 커스텀 설정 처럼 install_bff 없이 미들웨어를 직접 원하는 순서로 add_middleware 하세요.
고급 커스텀 설정
install_bff가 내부적으로 쓰는 팩토리를 직접 조립할 수도 있습니다. 미들웨어 순서·소유권을 완전히 제어하고 싶을 때 사용하세요.
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from axmp_oidc_bff_provider import (
BFFConfig, build_oauth, make_auth_router, make_proxy_router,
CSRFMiddleware, bff_lifespan,
)
config = BFFConfig.from_env()
app = FastAPI(lifespan=bff_lifespan(config)) # 세션 스토어 + httpx 클라이언트 관리
# 의존성/라우터가 런타임에 읽는 값
app.state.bff_config = config
app.state.bff_oauth = build_oauth(config)
# add_middleware는 나중에 추가한 게 바깥 → CSRF 먼저, Session 다음 (Session이 바깥)
app.add_middleware(CSRFMiddleware, config=config)
app.add_middleware(
SessionMiddleware,
secret_key=config.session_secret_key,
session_cookie=config.session_cookie_name,
same_site=config.session_same_site,
https_only=config.session_https_only,
max_age=config.session_ttl_seconds,
)
app.include_router(make_auth_router(config, app.state.bff_oauth))
proxy = make_proxy_router(config) # upstream 미설정 시 None
if proxy is not None:
app.include_router(proxy)
SessionMiddleware와CSRFMiddleware는 한 쌍입니다(CSRF가request.session을 읽음). 둘은 항상 함께, Session이 바깥에 오도록 추가하세요.
공개 API
from axmp_oidc_bff_provider import (
# 와이어링
install_bff, bff_lifespan,
# 설정
BFFConfig, OIDCProviderConfig, INSECURE_DEFAULT_SECRET,
# 팩토리 (고급 커스텀 설정용)
build_oauth, make_auth_router, make_proxy_router, CSRFMiddleware,
# CSRF 예외 경로 (빌트인 집합 + 매처)
EXEMPT_PATHS, is_csrf_exempt,
# 세션 스토어
create_session_store, SessionStore, InMemorySessionStore, RedisSessionStore,
# 의존성
get_current_user, get_current_user_optional, get_current_session,
# 유틸
SID_KEY, hash_sid,
)
패키지를 import 해도 부작용이 없습니다(env 읽기·OAuth 등록 없음). 심볼은 최초 접근 시 지연 로드됩니다(PEP 562).
데모 앱 실행
examples/main.py가 install_bff + 데모 라우트(/, /protected, /healthz)를 보여줍니다.
# 1) 의존성 + 로컬 스택(Keycloak + Redis)
uv sync
docker compose up -d
# 2) 환경변수
cp .env.example .env # OIDC_BFF_KEYCLOAK_*, OIDC_BFF_SESSION_SECRET_KEY 등 채우기
# 3) 서버 (examples/ 가 pythonpath)
uv run uvicorn main:app --app-dir examples --port 8000 --reload
OIDC_BFF_SESSION_SECRET_KEY는 python -c "import secrets; print(secrets.token_urlsafe(48))"로 생성하세요.
프로덕션 노트
- HTTPS:
OIDC_BFF_SESSION_HTTPS_ONLY=true(쿠키Secure). http 환경에서 true면 콜백 state 쿠키가 왕복되지 않아 로그인이 깨집니다. - Redis 필수(멀티 인스턴스):
OIDC_BFF_SESSION_BACKEND=redis로 모든 인스턴스가 세션을 공유. 인증이 필요한 Redis는OIDC_BFF_REDIS_URL=redis://:<password>@host:6379/0. - 시크릿 관리:
OIDC_BFF_SESSION_SECRET_KEY, 모든OIDC_BFF_*_CLIENT_SECRET을 시크릿 매니저(Vault/K8s Secret 등)로 주입. - 분리 호스트 SPA: SPA와 BFF가 다른 호스트면
OIDC_BFF_COOKIE_DOMAIN을 설정하고OIDC_BFF_FRONTEND_ORIGINS에 SPA 오리진을 추가(CORS credentialed). /healthz: Redis 백엔드일 때 ping하여 다운 시 503(degraded) 반환.
개발과 테스트
uv sync
uv run ruff check src/ examples/ tests/
uv run pytest -q
테스트는 라이브러리 컴포넌트(install_bff + 주입 BFFConfig)로 앱을 빌드하며, 데모 앱에 의존하지 않습니다.
Metadata
Release files for axmp-oidc-bff-provider 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| axmp_oidc_bff_provider-1.0.0.tar.gz | 204.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| axmp_oidc_bff_provider-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 278.0 kB
Release files / axmp_oidc_bff_provider-1.0.0.tar.gz
| Download URL | axmp_oidc_bff_provider-1.0.0.tar.gz |
|---|---|
| Size | 204.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ac576f1ddea695c664232799fcec80e2eacbe36ceebf2506cc8a001ed962b9a
|
|
BLAKE2b-256 checksum How to use checksums |
8baf212dd74b3ad72284beb9d5ef35f14227bafb49b762ac8bc5fb8f4fc78737
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / axmp_oidc_bff_provider-1.0.0-py3-none-any.whl
| Download URL | axmp_oidc_bff_provider-1.0.0-py3-none-any.whl |
|---|---|
| Size | 73.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8d46daa75157b89ad157c5bff92cc9bf0b0ecaf5415903d87bb205c2c9f8f82
|
|
BLAKE2b-256 checksum How to use checksums |
10406e13a3af18c758c8c2bf6da57bd43f049ca21810367106495e56b344df5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|