arvel-oauth¶
Introduction¶
arvel-oauth provides OAuth2/OIDC social login for Arvel. It handles the authorization-code flow (with PKCE and state cookies), links the external identity to a local user, and issues a JWT session via AuthService.
OAuth (Open Authorization) lets users log in with an external identity provider. OIDC (OpenID Connect) is an identity layer on top of OAuth2.
Installation¶
Register the provider and publish the migration:
# bootstrap/providers.py
from arvel_oauth import OAuthServiceProvider
providers = [OAuthServiceProvider]
OAuthServiceProvider binds OAuthConfig and OAuthManager as singletons and publishes the oauth_accounts table migration.
Supported Providers¶
| Name | Class | Notes |
|---|---|---|
google | GoogleProvider | OIDC userinfo; requests offline access |
github | GitHubProvider | Not OIDC; PKCE follows OAUTH_USE_PKCE (default on) |
microsoft | MicrosoftProvider | Entra ID; tenant from OAUTH_MICROSOFT_TENANT |
apple | AppleProvider | Uses a JWT client secret; identity from the verified id_token |
oidc | OIDCProvider | Generic; discovers config from the issuer's .well-known endpoint |
Configuration¶
OAuthConfig reads OAUTH_* environment variables. A provider counts as "configured" once its credentials are set — client id + secret for Google/GitHub/Microsoft, client id + private key for Apple, and issuer URL + client id for OIDC.
| Env var | Default |
|---|---|
OAUTH_USE_PKCE | true |
OAUTH_SUCCESS_REDIRECT_URL | / |
OAUTH_ERROR_REDIRECT_URL | /login |
OAUTH_ALLOW_HTTP_ISSUER | false |
OAUTH_GOOGLE_CLIENT_ID / _SECRET / _REDIRECT_URI | "" |
OAUTH_GITHUB_CLIENT_ID / _SECRET / _REDIRECT_URI | "" |
OAUTH_MICROSOFT_CLIENT_ID / _SECRET / _REDIRECT_URI / _TENANT | "" / common |
OAUTH_APPLE_CLIENT_ID / _TEAM_ID / _KEY_ID / _PRIVATE_KEY / _REDIRECT_URI | "" |
OAUTH_OIDC_ISSUER_URL / _CLIENT_ID / _CLIENT_SECRET / _REDIRECT_URI | "" |
Mounting the Routes¶
The package does not auto-mount routes. Build a controller and register the two endpoints yourself:
from fastapi import APIRouter
from arvel_oauth.http import OAuthController, register_oauth_routes
controller = OAuthController(manager=manager, config=config, auth=auth_service)
router = APIRouter()
register_oauth_routes(router, controller=controller, prefix="/auth")
app.include_router(router)
This registers:
GET /auth/{provider}/redirect— start the flow (sets state/PKCE cookies, redirects to the provider).GET /auth/{provider}/callback— exchange the code, link the account, issue a session, redirect to the success URL.
{provider} must be one of google, github, microsoft, apple, oidc.
Linking Accounts Directly¶
To handle the exchange yourself, use the linker — it finds or creates the local user and the oauth_accounts row:
from arvel_oauth import OAuthAccountLinker
account = await OAuthAccountLinker(session).link(oauth_user, token)
Data Model¶
OAuthAccount (table oauth_accounts) stores the link: user_id (FK to users.id), a unique (provider, provider_id), and the OAuth tokens encrypted via the Crypt facade.
Warning
Token encryption needs APP_KEY set when the column is read or written. Run arvel key:generate first.
Gotchas¶
InvalidOAuthStateis exported but isn't raised by the controller — a bad/missing state surfaces as aValidationException.- The Apple "configured" check looks at
client_id+private_keyonly; setteam_idandkey_idtoo for the flow to work. - The generic OIDC provider is resolved through
manager.oidc()(it performs discovery), not the synchronousmanager.provider("oidc").