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, 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-server and /.well-known/openid-configuration.
  • Client CRUD and consent CRUD RPC routes, including server-only /admin/oauth2/create-client and /admin/oauth2/update-client for 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/register when 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_url and consent_url are required whenever the authorization_code grant is enabled.
  • /auth/oauth2/create-client and /auth/oauth2/update-client only honor public client fields. Restricted fields such as skip_consent, enable_end_session, require_pkce, subject_type, metadata, and client_secret_expires_at belong on the server-only /auth/admin/oauth2/* routes.
  • /auth/oauth2/authorize ignores resource. Send resource to /auth/oauth2/token when requesting a resource-bound access token.
  • resource values sent to /auth/oauth2/token are validated against valid_audiences. Invalid resources return invalid_target.
  • Public clients and offline_access requests always require PKCE.
  • cached_trusted_clients and trusted_client_resolver can mark clients as trusted without allowing dynamic registration payloads to persist skip_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-client requires an authenticated session. Use /auth/oauth2/public-client-prelogin only when allow_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, and jwks_uri are not part of the persisted client surface.

JWT And OIDC Behavior

  • By default, access tokens are JWTs when a valid resource is requested at /auth/oauth2/token and opaque otherwise.
  • disable_jwt_plugin=True switches to non-JWT access tokens:
    • access tokens are always opaque
    • confidential clients still receive an id_token
    • the id_token is signed in HS256 with the client's secret
    • public clients do not receive an id_token
    • JWKS is not exposed
  • m2m_access_token_ttl_seconds lets 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_claims
  • custom_id_token_claims
  • custom_userinfo_claims
  • custom_token_response_fields

Reserved OAuth and OIDC fields are protected:

  • custom_access_token_claims cannot overwrite standard JWT claims
  • custom_id_token_claims cannot overwrite pinned OIDC claims
  • custom_token_response_fields cannot 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-client and /admin/oauth2/update-client.
  • In-config static OAuth client fields on OAuthServer were 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


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.19.2.tar.gz (64.8 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.19.2-py3-none-any.whl (79.6 kB view details)

Uploaded Python 3

File details

Details for the file belgie_oauth_server-0.19.2.tar.gz.

File metadata

  • Download URL: belgie_oauth_server-0.19.2.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

Hashes for belgie_oauth_server-0.19.2.tar.gz
Algorithm Hash digest
SHA256 665e238ef02c6128e156408629e9bacb6dfba43a3835fa44aae60e3a26960e33
MD5 f7978ba4682d6b4c733056482f6f4b32
BLAKE2b-256 aae13a047d771fa1b52e8bdd40ec891a923a4648bc162d2ca3a69f901ae658a2

See more details on using hashes here.

File details

Details for the file belgie_oauth_server-0.19.2-py3-none-any.whl.

File metadata

  • Download URL: belgie_oauth_server-0.19.2-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

Hashes for belgie_oauth_server-0.19.2-py3-none-any.whl
Algorithm Hash digest
SHA256 192c2419423542aeaee6f195ab070e7464477edb3ff268a36b7e8b30c1358e71
MD5 6ad6f6583ee06843b80cad672e6bfe84
BLAKE2b-256 cbc3b028bed2f819e23958a5ebc7cad003bbd381d66e74eaabe8bde93e3744ef

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