OAuth 2.1 authorization server for Belgie
Project description
Belgie OAuth Server
[!WARNING]
OAuthServer.adapteris required. Use a persistent adapter such asbelgie.alchemy.oauth_server.OAuthServerAdapterso clients, authorization state, authorization codes, access tokens, refresh tokens, and consents survive process restarts.
Belgie OAuth Server is Belgie's OAuth 2.1 and OpenID Connect provider package,
with a fixed /oauth2/* route layout and Pythonic naming.
Installation
uv add belgie-oauth-server
What It Provides
- Fixed
/oauth2/*routes for authorize, token, register, introspect, revoke, userinfo, and end-session. - OAuth and OIDC discovery metadata under
/.well-known/oauth-authorization-serverand/.well-known/openid-configuration. - Client CRUD and consent CRUD RPC routes, including server-only
/admin/oauth2/create-clientand/admin/oauth2/update-clientfor restricted client fields. - PKCE enforcement, pairwise subject identifiers, refresh-token rotation, prompt-aware login and consent flows, and dynamic client registration.
- Custom access-token claims, id-token claims, userinfo claims, and token response fields with reserved-field protections for standard OAuth/OIDC keys.
Provider-First Setup
The OAuth server no longer needs a baked-in static client. Configure the server first, then create clients through:
- authenticated client RPC routes such as
/auth/oauth2/create-client - server-only admin routes such as
/auth/admin/oauth2/create-client POST /auth/oauth2/registerwhen dynamic registration is enabled- server-side calls to
provider.register_client(...)
Quick Start
from collections.abc import AsyncGenerator
from typing import Annotated
from fastapi import Depends, FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from belgie import Belgie, BelgieClient, BelgieSettings
from belgie.alchemy import BelgieAdapter
from belgie.alchemy.oauth_server import OAuthServerAdapter
from belgie.oauth.server import OAuthLoginFlowClient, OAuthServer
app = FastAPI()
settings = BelgieSettings(
secret="change-me",
base_url="http://localhost:8000",
)
engine = create_async_engine("sqlite+aiosqlite:///./app.db")
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with session_maker() as session:
yield session
belgie = Belgie(
settings=settings,
adapter=BelgieAdapter(...),
database=get_db,
)
oauth_plugin = belgie.add_plugin(
OAuthServer(
adapter=OAuthServerAdapter(...),
base_url=settings.base_url,
login_url="/login",
consent_url="/consent",
signup_url="/signup",
valid_audiences=["http://localhost:8000/mcp"],
),
)
app.include_router(belgie.router)
@app.get("/login")
async def login(
request: Request,
oauth: Annotated[OAuthLoginFlowClient, Depends(oauth_plugin)],
) -> RedirectResponse:
context = await oauth.try_resolve_login_context(request)
if context is None:
return RedirectResponse("/", status_code=302)
return RedirectResponse(context.return_to, status_code=302)
@app.get("/consent")
async def consent(
request: Request,
oauth: Annotated[OAuthLoginFlowClient, Depends(oauth_plugin)],
) -> HTMLResponse:
context = await oauth.resolve_login_context(request)
return HTMLResponse(
f"""
<form method="post" action="/auth/oauth2/consent">
<input type="hidden" name="state" value="{context.state}" />
<input type="hidden" name="accept" value="true" />
<button type="submit">Approve</button>
</form>
"""
)
After the server is running, create clients through /auth/oauth2/create-client
or /auth/oauth2/register.
Important Behavior
login_urlandconsent_urlare required whenever theauthorization_codegrant is enabled./auth/oauth2/create-clientand/auth/oauth2/update-clientonly honor public client fields. Restricted fields such asskip_consent,enable_end_session,require_pkce,subject_type,metadata, andclient_secret_expires_atbelong on the server-only/auth/admin/oauth2/*routes./auth/oauth2/authorizeignoresresource. Sendresourceto/auth/oauth2/tokenwhen requesting a resource-bound access token.resourcevalues sent to/auth/oauth2/tokenare validated againstvalid_audiences. Invalid resources returninvalid_target.- Public clients and
offline_accessrequests always require PKCE. cached_trusted_clientsandtrusted_client_resolvercan mark clients as trusted without allowing dynamic registration payloads to persistskip_consent.- Consent is required for every non-trusted client until a matching consent record exists. Public PKCE clients are not implicitly trusted.
/auth/oauth2/public-clientrequires an authenticated session. Use/auth/oauth2/public-client-preloginonly whenallow_public_client_prelogin=True.- Trusted clients are immutable through the RPC routes. Update them in config or directly in persistence instead.
private_key_jwt,jwks, andjwks_uriare not part of the persisted client surface.
JWT And OIDC Behavior
- By default, access tokens are JWTs when a valid
resourceis requested at/auth/oauth2/tokenand opaque otherwise. disable_jwt_plugin=Trueswitches to non-JWT access tokens:- access tokens are always opaque
- confidential clients still receive an
id_token - the
id_tokenis signed inHS256with the client's secret - public clients do not receive an
id_token - JWKS is not exposed
m2m_access_token_ttl_secondslets machine-to-machine access tokens use a different default TTL than user tokens.
Protected Resource Metadata
Protected resources should publish their own
/.well-known/oauth-protected-resource document. The OAuth server does not own
that route.
Use build_protected_resource_metadata() to build the RFC 9728 document:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from belgie.oauth.server import OAuthServer, build_protected_resource_metadata
app = FastAPI()
oauth_settings = OAuthServer(...)
@app.get("/.well-known/oauth-protected-resource")
async def protected_resource_metadata() -> JSONResponse:
metadata = build_protected_resource_metadata(
"https://api.example.com/mcp",
settings=oauth_settings,
scopes_supported=["user"],
)
return JSONResponse(metadata.model_dump(mode="json", exclude_none=True))
Custom Claim Hooks
Belgie exposes these hook families with snake_case names:
custom_access_token_claimscustom_id_token_claimscustom_userinfo_claimscustom_token_response_fields
Reserved OAuth and OIDC fields are protected:
custom_access_token_claimscannot overwrite standard JWT claimscustom_id_token_claimscannot overwrite pinned OIDC claimscustom_token_response_fieldscannot overwrite standard token response fields
MCP Pairing
If you are protecting an MCP server, pair this package with belgie-mcp. The
MCP plugin consumes the OAuth metadata, token verifier behavior, and protected
resource metadata helper from this package.
Compatibility Notes
- OAuth server protocol routes are fixed to
/oauth2/*. - Restricted client fields are available through server-only
/admin/oauth2/create-clientand/admin/oauth2/update-client. - In-config static OAuth client fields on
OAuthServerwere removed; create clients through DCR, RPC, admin, or your adapter/seed. - The old auth-server-owned protected-resource metadata model is gone. Resource servers publish that metadata themselves.
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 belgie_oauth_server-0.19.3.tar.gz.
File metadata
- Download URL: belgie_oauth_server-0.19.3.tar.gz
- Upload date:
- Size: 64.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d2fab14131ab07be75510a2fd45808d6f41e2b3733dd74add175a525aa9242f
|
|
| MD5 |
b256fe25541c4c131683d6e5c615744b
|
|
| BLAKE2b-256 |
e7b38d0e9920051d75379be8851465dc39971302d1e129158d97f58dbc055949
|
File details
Details for the file belgie_oauth_server-0.19.3-py3-none-any.whl.
File metadata
- Download URL: belgie_oauth_server-0.19.3-py3-none-any.whl
- Upload date:
- Size: 79.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e57b041d6ed6bc5fc85197353705df25f1c319c251d45a9cd123863105854aa
|
|
| MD5 |
0601418a478acf91fb4e70451b358ee5
|
|
| BLAKE2b-256 |
960def3f2f91d2942f1f7ab2ecd7aeb647b71f05878fccbfe90197177750c786
|