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/GoogleOAuthClientMicrosoftOAuth/MicrosoftOAuthClientOAuthProviderOAuthPluginOAuthClientOAuthTokenSetOAuthLinkedAccountOAuthUserInfo
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_tokenrefresh_tokenaccess_token_expires_atrefresh_token_expires_attoken_typescopeid_token
Notes:
OAuthTokenSet.rawis 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_idas 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_postcallbacks - RFC 9207
issvalidation throughissuerandrequire_issuer_parameter_validation state_strategy="adapter"orstate_strategy="cookie"- sign-up gating
disable_sign_up=Truedisable_implicit_sign_up=Truedisable_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=Truetoken_encryption_secret=...
- provider hooks
get_tokenrefresh_tokensget_userinfomap_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
- 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=trueand 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_verifiedhandling - 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_staterequest.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.Authobject onBelgie
Examples
examples/oauth_client_plugin: Google client plugin flow, linked accounts, and the token expiry fieldsexamples/auth: end-to-end Belgie auth example usingGoogleOAuth(...)
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-0.19.2.tar.gz.
File metadata
- Download URL: belgie_oauth-0.19.2.tar.gz
- Upload date:
- Size: 29.5 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55030e8e8c7d5d5463865707993d2a5870afa78cf440b1287f8603348454e397
|
|
| MD5 |
9ca2fccff9f06609ebed8647b0bc0edf
|
|
| BLAKE2b-256 |
b08bae4d316d20897e076e204d057f6ee5e187ff6ceddde55010c6bcdd9c3ced
|
File details
Details for the file belgie_oauth-0.19.2-py3-none-any.whl.
File metadata
- Download URL: belgie_oauth-0.19.2-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
803116fa5ed78d15b7ce4026c5cad2816c344a74ede69bdb49c6ccb84bf77509
|
|
| MD5 |
e37cca31a58de76befe0809d87b2a57e
|
|
| BLAKE2b-256 |
780302c5544c24dafe6408cc959cd554a0adfe6f2ab4e288b9b1618e66a7fd80
|