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, interaction state, authorization codes, refresh tokens, and consents survive process restarts.
Belgie OAuth Server is the OAuth 2.1 authorization server package for Belgie apps. It gives you the server-side OAuth plumbing, metadata endpoints, PKCE handling, dynamic client registration, and prompt-aware login and consent flows without leaving the Python stack.
It is designed to pair with belgie-core and FastAPI. The package exposes a small settings object, a plugin, a client
helper for custom auth pages, and metadata builders for OAuth, OpenID Connect, and protected resource discovery.
Installation
uv add belgie-oauth-server
What It Covers
- OAuth 2.1 authorization, token, revoke, introspect, and userinfo routes.
- OpenID Connect metadata and
id_tokensupport. - OAuth protected resource metadata when you configure
resources=[OAuthServerResource(...)]. - Dynamic client registration, including the anonymous registration escape hatch when you explicitly enable it.
- Custom login, consent, and signup pages via
login_url,consent_url, andsignup_url.
Important Notes
- Resource matching is strict. If a client sends
resourceand no OAuth resource is configured, the server returnsinvalid_target. - If
authorization_codeis enabled,login_urlandconsent_urlare required. Belgie does not silently auto-consent by default. To mirror Better Auth's trusted-client behavior, usetrusted_client_resolverto let the server mark selected clients asskip_consentwithout allowingskip_consentin dynamic registration payloads. grant_typesdefaults to["authorization_code", "client_credentials", "refresh_token"]. If you disableauthorization_code,/authorizeis not mounted and metadata advertises nocoderesponse support.pairwise_secretis optional, but when you enable pairwise subject identifiers it must be at least 32 characters.- OAuth server persistence is adapter-backed. Static configured clients stay config-backed, while dynamic clients, interaction state, authorization codes, access tokens, refresh tokens, and consents live in the adapter.
allow_unauthenticated_client_registration=Trueis intentionally permissive. Treat it as a compatibility or development setting unless you have separate controls around registration. Anonymous registration always coerces clients totoken_endpoint_auth_method="none".
Examples
- Custom pages: prompt-aware login and signup routes with
OAuthServerClient. - MCP auth: OAuth server configuration paired with an MCP resource server.
Quick Start
Here is the smallest practical setup for a Belgie OAuth server with explicit login and consent pages:
Project Structure:
my-app/
├── server.py
└── views/
└── ...
server.py:
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 OAuthServer, OAuthServerClient
from yourapp.models import (
Account,
Individual,
OAuthServerAccessToken,
OAuthServerAuthorizationCode,
OAuthServerAuthorizationState,
OAuthServerClient as OAuthServerClientModel,
OAuthServerConsent,
OAuthServerRefreshToken,
OAuthState,
Session,
)
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
adapter = BelgieAdapter(
individual=Individual,
account=Account,
session=Session,
oauth_state=OAuthState,
)
oauth_adapter = OAuthServerAdapter(
oauth_client=OAuthServerClientModel,
oauth_authorization_state=OAuthServerAuthorizationState,
oauth_authorization_code=OAuthServerAuthorizationCode,
oauth_access_token=OAuthServerAccessToken,
oauth_refresh_token=OAuthServerRefreshToken,
oauth_consent=OAuthServerConsent,
)
belgie = Belgie(settings=settings, adapter=adapter, database=get_db)
oauth_plugin = belgie.add_plugin(
OAuthServer(
adapter=oauth_adapter,
base_url=settings.base_url,
client_id="demo-client",
client_secret="demo-secret",
redirect_uris=["http://localhost:3030/callback"],
login_url="/login",
consent_url="/consent",
signup_url="/signup",
),
)
app.include_router(belgie.router)
@app.get("/login")
async def login(
request: Request,
oauth: Annotated[OAuthServerClient, Depends(oauth_plugin)],
) -> RedirectResponse:
context = await oauth.try_resolve_login_context(request)
if context is None:
return RedirectResponse(url="/login/google", status_code=302)
if context.intent == "create":
return RedirectResponse(url=f"/signup?state={context.state}", status_code=302)
return RedirectResponse(url=f"/login/google?state={context.state}", status_code=302)
@app.get("/consent")
async def consent(
request: Request,
oauth: Annotated[OAuthServerClient, Depends(oauth_plugin)],
) -> HTMLResponse:
context = await oauth.resolve_login_context(request)
return HTMLResponse(
f"""
<form method="post" action="/auth/oauth/consent">
<input type="hidden" name="state" value="{context.state}" />
<input type="hidden" name="accept" value="true" />
<button type="submit">Approve</button>
</form>
"""
)
@app.get("/signup")
async def signup(
request: Request,
oauth: Annotated[OAuthServerClient, Depends(oauth_plugin)],
client: Annotated[BelgieClient, Depends(belgie)],
) -> RedirectResponse:
context = await oauth.resolve_login_context(request)
response = RedirectResponse(url=context.return_to, status_code=302)
_user, session = await client.sign_up("dev@example.com", request=request)
return client.create_session_cookie(session, response)
Run the app with:
uv run uvicorn server:app --reload
Configuration
adapteris required and is responsible for persisting OAuth server state.prefixcontrols where the OAuth server routes are mounted. The default is/oauth.base_urlis used to derive issuer and metadata URLs.redirect_urisis required and must contain at least one callback URL.grant_typescontrols which server grants are active. The default is["authorization_code", "client_credentials", "refresh_token"].refresh_tokencannot be enabled unlessauthorization_codeis also enabled.login_urlandconsent_urlare required whenauthorization_codeis enabled.signup_urlis optional.prompt=createuses it when present and otherwise falls back tologin_url.pairwise_secretenables pairwise subject identifiers and must be at least 32 characters.resources=[OAuthServerResource(prefix=..., scopes=...)]enables protected resource metadata.enable_end_sessionturns on RP-initiated logout support.allow_dynamic_client_registrationenablesPOST /auth/oauth/register.- Authenticated dynamic registration defaults to:
token_endpoint_auth_method="client_secret_basic"for confidential clients.grant_types=["authorization_code"]when the client omitsgrant_types.
allow_unauthenticated_client_registrationlets anonymous callers register clients without authentication, but those registrations are always coerced to public clients withtoken_endpoint_auth_method="none"and cannot requestclient_credentials.- Registered client
grant_typesmust be a subset of the server-levelgrant_types.
Login Flow
- Auth-code servers are interactive by design: unauthenticated requests go through
login_url, missing consent goes throughconsent_url, andprompt=nonereturns protocol errors instead of redirecting to UI. prompt=createpreferssignup_urlwhen it is configured.- Otherwise
login_urlis used. OAuthServerClient.try_resolve_login_context(request)returnsNonewhen no OAuth state is present, which makes it easy to support both direct visits and redirect-driven entry points.
Advanced Capabilities
request_uri_resolverlets you resolve pushed or out-of-band authorization parameters before request validation.- Client and consent management routes are built in under the OAuth server prefix.
allow_public_client_preloginenables public-client lookup before login for custom UX.rate_limitexposes per-endpoint rate limiting for authorize, token, registration, introspection, revoke, and userinfo.custom_access_token_claims,custom_id_token_claims,custom_userinfo_claims, andcustom_token_response_fieldslet you inject product-specific claims and token response fields.- Protected resource metadata is exposed automatically when you configure
resources=[OAuthServerResource(...)], with optional root well-known fallbacks controlled by the metadata fallback settings.
Migration Note
route_prefixhas been removed. Useprefixinstead.resource_server_urlhas been removed. Useresources=[OAuthServerResource(...)]instead.resource_scopeshas been removed. Put scopes onOAuthServerResource(scopes=[...])instead.
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.16.2.tar.gz.
File metadata
- Download URL: belgie_oauth_server-0.16.2.tar.gz
- Upload date:
- Size: 45.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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 |
0be864ac82445d3265364caa36c53eb9e8a2e8df5c493846633b24a493c53f59
|
|
| MD5 |
c216d77ce7c9a238006158ef5eb0e629
|
|
| BLAKE2b-256 |
f6f38d9b1780382ec9b905ac293e3907a25be526723a38a68d90ef90e956baae
|
File details
Details for the file belgie_oauth_server-0.16.2-py3-none-any.whl.
File metadata
- Download URL: belgie_oauth_server-0.16.2-py3-none-any.whl
- Upload date:
- Size: 51.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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 |
2eb4f5f99a764ecf2f17c38a7af997c46ad4af461f93e0bb7cd13c0da81bba9f
|
|
| MD5 |
0557d0c3984a51e89708cc76d1dac033
|
|
| BLAKE2b-256 |
da0984138900ee2e87bb1a5e04a8a51728dff7cf6efcfc974f38d4a8949fb5c7
|