Skip to main content

FastAPI + Authlib BFF backend for Keycloak OIDC login

Project description

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 가 있을 때)
  • 라우터: /auth/*(로그인·콜백·로그아웃·me), /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 /auth/login?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"}}}

런타임에는 /auth/login?provider=dream 으로 선택합니다. 키 이름이 빌트인과 겹치면 커스텀이 우선합니다.


제공 엔드포인트

install_bff / make_auth_router/auth 프리픽스로 추가하는 라우트:

Method Path 설명
GET /auth/login?provider=<name>&next=<url> 선택한 IdP로 302 리다이렉트(PKCE). provider 생략 시 기본값. nextfrontend_origins 내에서만 허용
GET /auth/callback 인가 코드 교환 → 서버사이드 세션(sid) 생성 → next/frontend_url로 리다이렉트
GET /auth/logout 로컬 세션 삭제 + IdP RP-initiated logout
GET /auth/me 현재 사용자 OIDC 클레임(JSON), 인증 필요

IdP/discovery 장애 시 /auth/login은 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)로 앱을 빌드하며, 데모 앱에 의존하지 않습니다.

Project details


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.9.1.tar.gz (164.3 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.9.1-py3-none-any.whl (56.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: axmp_oidc_bff_provider-0.9.1.tar.gz
  • Upload date:
  • Size: 164.3 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.9.1.tar.gz
Algorithm Hash digest
SHA256 6ea06c22249d772af238d7a5fa05aa6fe6e3570b33241b1eac76c60066dcb2f9
MD5 1bd619fa310c850e3228323951a9150f
BLAKE2b-256 0340f5ed03ed4fb11d257ab0814d71596c8201d084418e786ba7c48a500edb90

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for axmp_oidc_bff_provider-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 58110c007acceeac1a4ecef1546d1863b0240fc607ccd31a22d6caf9cbf08bb8
MD5 3b1642398538a3b7beef397438fecd94
BLAKE2b-256 cbcabc7d21303e6cbd0e232600d459e4425e8508a6bb88c762252f511b1127cf

See more details on using hashes here.

Supported by

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