Skip to main content

OAuth client plugins for Belgie

Project description

belgie-oauth: OAuth/OIDC for Belgie

[!WARNING] This package is still part of Belgie's beta API surface.

belgie-oauth is Belgie's Authlib-backed OAuth 2.0 / OpenID Connect client runtime. It keeps the public integration centered on:

  • GoogleOAuth / GoogleOAuthClient
  • MicrosoftOAuth / MicrosoftOAuthClient
  • OAuthProvider
  • OAuthPlugin
  • OAuthClient
  • OAuthTokenSet
  • OAuthLinkedAccount
  • OAuthUserInfo

Provider presets are the primary API. OAuthProvider remains public as the advanced custom-provider escape hatch.

Internally, the runtime is organized around these layers:

  • transport: discovery, authorization URL generation, code exchange, refresh, and Authlib-backed ID-token validation
  • provider strategy: provider-owned auth request defaults, userinfo/profile resolution, and refresh specialization
  • state: adapter-backed or cookie-backed OAuth state handling
  • flow: sign-in, account linking, callback orchestration, redirects, and persistence updates

It intentionally uses Authlib's low-level AsyncOAuth2Client + AsyncOpenIDMixin. It does not use authlib.integrations.starlette_client.OAuth, because Belgie owns state and cookies instead of relying on session-middleware state.

Installation

uv add belgie-oauth

You also need normal Belgie configuration, including BELGIE_SECRET and BELGIE_BASE_URL.

Quick Start

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import SecretStr

from belgie import Belgie, BelgieSettings
from belgie_oauth import GoogleOAuth, GoogleOAuthClient

settings = BelgieSettings(
    secret="your-secret-key",
    base_url="http://localhost:8000",
)

auth = Belgie(
    settings=settings,
    adapter=adapter,
    database=get_db,
)

google = auth.add_plugin(
    GoogleOAuth(
        client_id="your-google-client-id",
        client_secret=SecretStr("your-google-client-secret"),
        scopes=["openid", "email", "profile"],
    )
)

app = FastAPI()
app.include_router(auth.router)


@app.get("/login/google")
async def login_google(
    oauth: Annotated[GoogleOAuthClient, Depends(google)],
    return_to: str | None = None,
):
    url = await oauth.signin_url(
        return_to=return_to,
        error_redirect_url="/auth/error",
        new_user_redirect_url="/welcome",
        payload={"source": "marketing-site"},
        request_sign_up=True,
    )
    return RedirectResponse(url=url, status_code=302)

Register this callback URL with the provider:

http://localhost:8000/auth/provider/google/callback

signin_url(...) and link_url(...) now return a Belgie-owned start URL first. That local start route sets the required state cookie or marker and then redirects to the provider. The app-owned login route stays the same.

Account Operations

Provider clients expose both flow helpers and post-sign-in account operations:

from typing import Annotated

from fastapi import Depends

from belgie_oauth import GoogleOAuthClient


@app.get("/accounts/google")
async def list_google_accounts(
    oauth: Annotated[GoogleOAuthClient, Depends(google)],
    user: Annotated[Individual, Depends(auth.individual)],
):
    return await oauth.list_accounts(individual_id=user.id)


@app.get("/accounts/google/token-set")
async def google_token_set(
    oauth: Annotated[GoogleOAuthClient, Depends(google)],
    user: Annotated[Individual, Depends(auth.individual)],
    provider_account_id: str,
):
    token_set = await oauth.token_set(
        individual_id=user.id,
        provider_account_id=provider_account_id,
    )
    return {
        "access_token": token_set.access_token,
        "access_token_expires_at": (
            token_set.access_token_expires_at.isoformat() if token_set.access_token_expires_at else None
        ),
        "refresh_token_expires_at": (
            token_set.refresh_token_expires_at.isoformat() if token_set.refresh_token_expires_at else None
        ),
    }


@app.get("/accounts/google/access-token")
async def google_access_token(
    oauth: Annotated[GoogleOAuthClient, Depends(google)],
    user: Annotated[Individual, Depends(auth.individual)],
    provider_account_id: str,
):
    return {
        "access_token": await oauth.get_access_token(
            individual_id=user.id,
            provider_account_id=provider_account_id,
        )
    }

Available dependency methods:

  • signin_url(...)
  • link_url(...)
  • list_accounts(individual_id=...)
  • token_set(individual_id=..., provider_account_id=..., auto_refresh=True)
  • get_access_token(individual_id=..., provider_account_id=..., auto_refresh=True)
  • refresh_account(individual_id=..., provider_account_id=...)
  • account_info(individual_id=..., provider_account_id=..., auto_refresh=True)
  • unlink_account(individual_id=..., provider_account_id=...)

When the provider plugin is injected as a FastAPI dependency, provider_account_id can be omitted for token, refresh, account-info, and unlink operations if store_account_cookie=True and the browser has a matching encrypted account cookie. Explicit provider_account_id values remain authoritative.

The plugin also exposes provider-owned JSON routes:

POST /auth/provider/{provider_id}/signin/id-token
POST /auth/provider/{provider_id}/link/id-token
GET  /auth/provider/{provider_id}/accounts
POST /auth/provider/{provider_id}/unlink
POST /auth/provider/{provider_id}/access-token
POST /auth/provider/{provider_id}/refresh-token
GET  /auth/provider/{provider_id}/account-info

Direct ID-token sign-in returns JSON with redirect=False, a Belgie session token, and the serialized individual while also setting the normal Belgie session cookie.

Persistence Model

OAuth account persistence is a clean break from the older single-expiry layout.

Stored account fields now include:

  • access_token
  • refresh_token
  • access_token_expires_at
  • refresh_token_expires_at
  • token_type
  • scope
  • id_token

Notes:

  • OAuthTokenSet.raw is still available at runtime for provider-specific responses.
  • Raw token JSON is not required to be persisted.
  • When refresh responses omit optional fields, the runtime preserves the existing stored values where that is safe to do so.
  • Optional token encryption still works at rest through encrypt_tokens=True.

State Strategies

OAuthProvider.state_strategy controls where Belgie stores OAuth state.

  • adapter
    • Persists the state row through the Belgie adapter.
    • Also sets a short-lived signed marker cookie so the callback is still bound to the initiating browser.
  • cookie
    • Stores the full transient state in a short-lived encrypted cookie.
    • Does not create adapter rows for OAuth state.

Both strategies keep the callback route shape at:

GET|POST /auth/provider/{provider_id}/callback

form_post callbacks are normalized before validation so cookie-backed state remains usable even when the browser does not send the cookie on the initial cross-site POST.

Custom Providers

Provider presets are the primary API. Use OAuthProvider when you need a custom or non-built-in provider.

OAuthProvider supports:

  • OIDC discovery or manual authorization_endpoint / token_endpoint / userinfo_endpoint / jwks_uri
  • client_id as either a single string or an ordered list of accepted client IDs
    • Belgie uses the first entry for authorization requests and token exchange
    • Belgie accepts any configured entry when validating OIDC ID-token audiences
  • PKCE on or off
  • query-mode or form_post callbacks
  • RFC 9207 iss validation through issuer and require_issuer_parameter_validation
  • state_strategy="adapter" or state_strategy="cookie"
  • sign-up gating
    • disable_sign_up=True
    • disable_implicit_sign_up=True
    • disable_id_token_sign_in=True
  • profile refresh for existing linked users
    • override_user_info_on_sign_in=True
  • optional account-cookie lookup
    • store_account_cookie=True
  • default callback error redirects
    • default_error_redirect_url="/auth/error"
  • optional token encryption
    • encrypt_tokens=True
    • token_encryption_secret=...
  • provider hooks
    • get_token
    • refresh_tokens
    • get_userinfo
    • map_profile

Example with manual endpoints and cookie-backed state:

provider = OAuthProvider(
    provider_id="acme",
    client_id="acme-client-id",
    client_secret=SecretStr("acme-client-secret"),
    authorization_endpoint="https://idp.example.com/oauth2/authorize",
    token_endpoint="https://idp.example.com/oauth2/token",
    userinfo_endpoint="https://idp.example.com/userinfo",
    issuer="https://idp.example.com",
    scopes=["openid", "email", "profile"],
    use_pkce=True,
    state_strategy="cookie",
    disable_sign_up=True,
    override_user_info_on_sign_in=True,
    store_account_cookie=True,
)

Public-client providers are first-class. If client_secret=None, Belgie will use token_endpoint_auth_method="none". Cross-platform OIDC apps can also provide client_id=["web-client-id", "ios-client-id"] when the provider issues ID tokens for multiple accepted audiences.

Presets

Google and Microsoft presets are the intended default entrypoints:

from belgie_oauth import GoogleOAuth, MicrosoftOAuth

Google

  • OIDC discovery through https://accounts.google.com/.well-known/openid-configuration
  • default scopes: openid email profile
  • default prompt="consent"
  • default access_type="offline"
  • include_granted_scopes=true and optional hosted-domain routing
  • shared options from the generic runtime, including state_strategy, PKCE, nonce, response_mode, and token params
  • ID-token validation first, userinfo fallback only when needed

Microsoft

  • tenant-aware authorize, token, and JWKS endpoints
  • default scopes: openid profile email offline_access User.Read
  • callback issuer validation only for tenant-specific issuers
  • JWKS-backed ID-token verification through Authlib
  • Graph OIDC userinfo support
  • optional Graph photo enrichment
  • conservative email_verified handling
  • public-client mode works with client_secret=None
  • shared options from the generic runtime, including state_strategy, PKCE, nonce, response_mode, and token params

Hook Visibility

During successful callback handling, Belgie exposes:

  • request.state.oauth_state
  • request.state.oauth_payload

This keeps payload passthrough and callback metadata available to hooks like after_authenticate.

Design Notes

Belgie's OAuth client runtime keeps provider transport, state handling, linked accounts, direct ID-token flows, account APIs, optional account-cookie lookup, and token refresh inside the plugin runtime.

Belgie uses Authlib's low-level AsyncOAuth2Client + AsyncOpenIDMixin. It does not use authlib.integrations.starlette_client.OAuth, because Belgie owns state and cookies instead of relying on session-middleware state.

  • Belgie does not expose Authlib session middleware or a public authlib.OAuth / authlib.Auth object on Belgie

Examples

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-0.19.0.tar.gz (29.6 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-0.19.0-py3-none-any.whl (38.9 kB view details)

Uploaded Python 3

File details

Details for the file belgie_oauth-0.19.0.tar.gz.

File metadata

  • Download URL: belgie_oauth-0.19.0.tar.gz
  • Upload date:
  • Size: 29.6 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-0.19.0.tar.gz
Algorithm Hash digest
SHA256 56dbd4470132c03367b63903445a9b2c1cd902802eec1e59f342c34e1a852765
MD5 c0ce3ac6438bf9834ff4d45ad9534047
BLAKE2b-256 8c8d870b495928e23e4329731485e044677c57f2656dd4765930c22b0b2d3b1e

See more details on using hashes here.

File details

Details for the file belgie_oauth-0.19.0-py3-none-any.whl.

File metadata

  • Download URL: belgie_oauth-0.19.0-py3-none-any.whl
  • Upload date:
  • Size: 38.9 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-0.19.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d15e19cb6e19baeb15cec731fcec0d893777d7bc3a007d6411c04064f5494e1b
MD5 07358095b2056c5d97ff2494947c37df
BLAKE2b-256 593e354a0a2bac9d5fc6dd6dfa60479a25c012de52b15c05dae07652b2a0bb8e

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