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 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 and signup pages via
login_urlandsignup_url.
Important Notes
- Resource matching is strict. If a client sends
resourceand no OAuth resource is configured, the server returnsinvalid_target. - 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.
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 custom login 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 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",
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("/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.resources=[OAuthServerResource(prefix=..., scopes=...)]enables protected resource metadata.enable_end_sessionturns on RP-initiated logout support.allow_dynamic_client_registrationenablesPOST /auth/oauth/register.allow_unauthenticated_client_registrationlets anonymous callers register clients without authentication.
Login Flow
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.
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.15.2.tar.gz.
File metadata
- Download URL: belgie_oauth_server-0.15.2.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","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 |
c5fac3964803627fd953574f96414150011811a2c4f113ab8827fc43d4f559a4
|
|
| MD5 |
944f33e77fe60ede1c8681118e62d856
|
|
| BLAKE2b-256 |
ea40a8044f002c03df022a746584aaa53e7bdf5357da70e471b292cb33f850cf
|
File details
Details for the file belgie_oauth_server-0.15.2-py3-none-any.whl.
File metadata
- Download URL: belgie_oauth_server-0.15.2-py3-none-any.whl
- Upload date:
- Size: 33.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","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 |
a35191d2c5e312b80e1b8bf809b66fc964eebabcccc72c2b6ee42456acaf91bc
|
|
| MD5 |
356ce327637fb69fa7eda5b5577246b5
|
|
| BLAKE2b-256 |
fba36e55e9f92961f55bc07a86ecb527217f339a6356d4f6953e6689eb8a4a25
|