FastAPI + Authlib BFF backend for Keycloak OIDC login
Project description
소비하는 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)를 코드 수정 없이
BFF_OIDC_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가 있을 때) - 라우터:
/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 변수는 BFF_OIDC_ 프리픽스를 씁니다(소비 앱의 다른 설정과 충돌 방지). 즉 env 변수명 = BFF_OIDC_ + 필드명 대문자 (예: default_provider → BFF_OIDC_DEFAULT_PROVIDER, keycloak_base_url → BFF_OIDC_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 변수는
BFF_OIDC_프리픽스를 씁니다.
| Provider | 핵심 설정 |
|---|---|
| keycloak | BFF_OIDC_KEYCLOAK_BASE_URL, BFF_OIDC_KEYCLOAK_REALM, BFF_OIDC_KEYCLOAK_CLIENT_ID, BFF_OIDC_KEYCLOAK_CLIENT_SECRET |
| okta | BFF_OIDC_OKTA_ORG_URL, BFF_OIDC_OKTA_CLIENT_ID, BFF_OIDC_OKTA_CLIENT_SECRET |
BFF_OIDC_GOOGLE_CLIENT_ID, BFF_OIDC_GOOGLE_CLIENT_SECRET |
|
| azure | BFF_OIDC_AZURE_TENANT_ID, BFF_OIDC_AZURE_CLIENT_ID, BFF_OIDC_AZURE_CLIENT_SECRET |
| github | BFF_OIDC_GITHUB_CLIENT_ID, BFF_OIDC_GITHUB_CLIENT_SECRET (비-OIDC OAuth2; userinfo는 api.github.com에서 조회) |
Keycloak이 구버전(레거시
/auth컨텍스트 경로)이면BFF_OIDC_KEYCLOAK_BASE_URL에/auth를 포함하세요(예:https://kc.example.com/auth).
커스텀 (표준 OIDC IdP — 코드 수정 불필요)
.well-known/openid-configuration discovery 문서를 노출하는 IdP는 BFF_OIDC_PROVIDERS(JSON)로 추가합니다:
BFF_OIDC_DEFAULT_PROVIDER=dream
BFF_OIDC_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 생략 시 기본값. next는 frontend_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)
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,
# 세션 스토어
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 # BFF_OIDC_KEYCLOAK_*, BFF_OIDC_SESSION_SECRET_KEY 등 채우기
# 3) 서버 (examples/ 가 pythonpath)
uv run uvicorn main:app --app-dir examples --port 8000 --reload
BFF_OIDC_SESSION_SECRET_KEY는 python -c "import secrets; print(secrets.token_urlsafe(48))"로 생성하세요.
프로덕션 노트
- HTTPS:
BFF_OIDC_SESSION_HTTPS_ONLY=true(쿠키Secure). http 환경에서 true면 콜백 state 쿠키가 왕복되지 않아 로그인이 깨집니다. - Redis 필수(멀티 인스턴스):
BFF_OIDC_SESSION_BACKEND=redis로 모든 인스턴스가 세션을 공유. 인증이 필요한 Redis는BFF_OIDC_REDIS_URL=redis://:<password>@host:6379/0. - 시크릿 관리:
BFF_OIDC_SESSION_SECRET_KEY, 모든BFF_OIDC_*_CLIENT_SECRET을 시크릿 매니저(Vault/K8s Secret 등)로 주입. - 분리 호스트 SPA: SPA와 BFF가 다른 호스트면
BFF_OIDC_COOKIE_DOMAIN을 설정하고BFF_OIDC_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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file axmp_oidc_bff_provider-0.9.0.tar.gz.
File metadata
- Download URL: axmp_oidc_bff_provider-0.9.0.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72f6fc4103ab33bf1f544857d071d635e486b7e993759d39ca49862a7e3a19b8
|
|
| MD5 |
5f7d3358b0a4f2a5510a8dbc44d14763
|
|
| BLAKE2b-256 |
486a5b13afc511e568a5d702c6d54fb27220bcbeb0094525209aefa22e117a03
|
File details
Details for the file axmp_oidc_bff_provider-0.9.0-py3-none-any.whl.
File metadata
- Download URL: axmp_oidc_bff_provider-0.9.0-py3-none-any.whl
- Upload date:
- Size: 56.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
835ad7ebe49dcbb10b35b223a2b547595f6bf0f9228bf6d6f6eb27b40658a992
|
|
| MD5 |
a32f5c4d98cfc02c1ff2d378e6db41d7
|
|
| BLAKE2b-256 |
b35d17603f74efbe5d7a00745b623c0aa7476e14e4527fa9dc3d48d2179de15d
|