Skip to main content

OAuth 2.1 authorization server for Belgie

Project description

Belgie OAuth Server

[!WARNING] OAuthServer.adapter is required. Use a persistent adapter such as belgie.alchemy.oauth_server.OAuthServerAdapter so 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_token support.
  • 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, and signup_url.

Important Notes

  • Resource matching is strict. If a client sends resource and no OAuth resource is configured, the server returns invalid_target.
  • If authorization_code is enabled, login_url and consent_url are required. Belgie does not silently auto-consent by default. To mirror Better Auth's trusted-client behavior, use trusted_client_resolver to let the server mark selected clients as skip_consent without allowing skip_consent in dynamic registration payloads.
  • grant_types defaults to ["authorization_code", "client_credentials", "refresh_token"]. If you disable authorization_code, /authorize is not mounted and metadata advertises no code response support.
  • pairwise_secret is 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=True is intentionally permissive. Treat it as a compatibility or development setting unless you have separate controls around registration. Anonymous registration always coerces clients to token_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

  • adapter is required and is responsible for persisting OAuth server state.
  • prefix controls where the OAuth server routes are mounted. The default is /oauth.
  • base_url is used to derive issuer and metadata URLs.
  • redirect_uris is required and must contain at least one callback URL.
  • grant_types controls which server grants are active. The default is ["authorization_code", "client_credentials", "refresh_token"].
  • refresh_token cannot be enabled unless authorization_code is also enabled.
  • login_url and consent_url are required when authorization_code is enabled.
  • signup_url is optional. prompt=create uses it when present and otherwise falls back to login_url.
  • pairwise_secret enables pairwise subject identifiers and must be at least 32 characters.
  • resources=[OAuthServerResource(prefix=..., scopes=...)] enables protected resource metadata.
  • enable_end_session turns on RP-initiated logout support.
  • allow_dynamic_client_registration enables POST /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 omits grant_types.
  • allow_unauthenticated_client_registration lets anonymous callers register clients without authentication, but those registrations are always coerced to public clients with token_endpoint_auth_method="none" and cannot request client_credentials.
  • Registered client grant_types must be a subset of the server-level grant_types.

Login Flow

  • Auth-code servers are interactive by design: unauthenticated requests go through login_url, missing consent goes through consent_url, and prompt=none returns protocol errors instead of redirecting to UI.
  • prompt=create prefers signup_url when it is configured.
  • Otherwise login_url is used.
  • OAuthServerClient.try_resolve_login_context(request) returns None when no OAuth state is present, which makes it easy to support both direct visits and redirect-driven entry points.

Advanced Capabilities

  • request_uri_resolver lets 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_prelogin enables public-client lookup before login for custom UX.
  • rate_limit exposes per-endpoint rate limiting for authorize, token, registration, introspection, revoke, and userinfo.
  • custom_access_token_claims, custom_id_token_claims, custom_userinfo_claims, and custom_token_response_fields let 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_prefix has been removed. Use prefix instead.
  • resource_server_url has been removed. Use resources=[OAuthServerResource(...)] instead.
  • resource_scopes has been removed. Put scopes on OAuthServerResource(scopes=[...]) instead.

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

belgie_oauth_server-0.16.2.tar.gz (45.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

belgie_oauth_server-0.16.2-py3-none-any.whl (51.0 kB view details)

Uploaded Python 3

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

Hashes for belgie_oauth_server-0.16.2.tar.gz
Algorithm Hash digest
SHA256 0be864ac82445d3265364caa36c53eb9e8a2e8df5c493846633b24a493c53f59
MD5 c216d77ce7c9a238006158ef5eb0e629
BLAKE2b-256 f6f38d9b1780382ec9b905ac293e3907a25be526723a38a68d90ef90e956baae

See more details on using hashes here.

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

Hashes for belgie_oauth_server-0.16.2-py3-none-any.whl
Algorithm Hash digest
SHA256 2eb4f5f99a764ecf2f17c38a7af997c46ad4af461f93e0bb7cd13c0da81bba9f
MD5 0557d0c3984a51e89708cc76d1dac033
BLAKE2b-256 da0984138900ee2e87bb1a5e04a8a51728dff7cf6efcfc974f38d4a8949fb5c7

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