Skip to main content

axmp-oidc-bff-provider

FastAPI + Authlib 기반 OIDC BFF(Backend-for-Frontend) 라이브러리

Python FastAPI Authlib Lint: Ruff

소비하는 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-in BFFConfig.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
google 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)

Source distribution for axmp-oidc-bff-provider 1.0.0
File Size Uploaded
axmp_oidc_bff_provider-1.0.0.tar.gz 204.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for axmp-oidc-bff-provider 1.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.11.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page