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_providerOIDC_BFF_DEFAULT_PROVIDER, keycloak_base_urlOIDC_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 가능)
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).


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로 리다이렉트
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 처리됩니다.

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)

SessionMiddlewareCSRFMiddleware는 한 쌍입니다(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,
    # 세션 스토어
    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.pyinstall_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_KEYpython -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)로 앱을 빌드하며, 데모 앱에 의존하지 않습니다.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

axmp_oidc_bff_provider-0.10.0.tar.gz (165.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

axmp_oidc_bff_provider-0.10.0-py3-none-any.whl (56.4 kB view details)

Uploaded Python 3

File details

Details for the file axmp_oidc_bff_provider-0.10.0.tar.gz.

File metadata

  • Download URL: axmp_oidc_bff_provider-0.10.0.tar.gz
  • Upload date:
  • Size: 165.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for axmp_oidc_bff_provider-0.10.0.tar.gz
Algorithm Hash digest
SHA256 ef23bc0ec0f2daa0cf02fd8c9af31f068ba37cb1c565ea9b5b1cfce55a73b9a4
MD5 eebfe5a542332d3a9ac500a2d53d163f
BLAKE2b-256 40d0e062a0c417d17c2bba62a88dec5122aeb6d674cad4059dedf583c0f572bd

See more details on using hashes here.

File details

Details for the file axmp_oidc_bff_provider-0.10.0-py3-none-any.whl.

File metadata

File hashes

Hashes for axmp_oidc_bff_provider-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 203222cbed1564792b316d8c6491d6031b7f7b2c6db8593975f6fb0f081c8f09
MD5 b7eacbf62b7e8be9807f9a6affce90c9
BLAKE2b-256 a1d1a4547b48677dc5e6d9626c380f46a10b837ff13f42de9948dd9df821ce73

See more details on using hashes here.

Release history Release notifications | RSS feed

0.12.0

2 files

0.11.0

2 files

This release

0.10.0 This release

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page