OAuth¶
FastAuth ships Google and GitHub providers. Both require the oauth extra (httpx). Account-linking endpoints let authenticated users connect additional providers without a fresh sign-in.
Providers¶
from fastauth.providers.google import GoogleProvider
from fastauth.providers.github import GitHubProvider
GoogleProvider(client_id="...", client_secret="...", scopes=None) # default scopes: openid, email, profile
GitHubProvider(client_id="...", client_secret="...", scopes=None) # default scopes: user:email
Both implement OAuthProvider (core.protocols). GitHub fetches the primary verified email from /user/emails when /user does not include one.
Setup (config)¶
from fastauth.session_backends.memory import MemorySessionBackend
config = FastAuthConfig(
...,
providers=[
CredentialsProvider(),
GoogleProvider(client_id=..., client_secret=...),
GitHubProvider(client_id=..., client_secret=...),
],
oauth_adapter=adapter.oauth, # required for all OAuth endpoints
oauth_state_store=MemorySessionBackend(), # required for OAuth (Redis in prod)
oauth_redirect_url="https://app.example.com/auth/callback",# frontend post-callback URL
oauth_allowed_redirect_uris=[ # exact URLs allowed in redirect_uri
"http://localhost:8000/auth/oauth/google/callback",
"http://localhost:8000/auth/oauth/github/callback",
],
# store_oauth_provider_tokens=True, # persist provider access/refresh tokens (default False)
)
oauth_state_storestores OAuth CSRF + PKCE state (TTL 600s). UseRedisSessionBackendin production so multi-process deployments share state.oauth_redirect_urlis the frontend URL FastAuth 302s to after a successful callback. The access/refresh tokens are set asHttpOnlycookies on that response, never appended to the URL.oauth_allowed_redirect_urisis the allowlist of exact provider-callback URLs the user may put inredirect_urion the authorize endpoint.None(default) accepts anything — set this in production.
Flow¶
sequenceDiagram
participant FE as Frontend
participant BE as FastAuth app
participant G as Google/GitHub
FE->>BE: GET /auth/oauth/google/authorize?redirect_uri=...callback
BE-->>G: store state+PKCE
BE-->>FE: 200 {url}
FE->>G: browser redirects to url
G-->>BE: GET /auth/oauth/google/callback?code=...&state=...
BE->>G: exchange code for tokens (PKCE)
BE->>G: GET /userinfo
BE->>BE: find/create user, link OAuth account
BE-->>FE: 302 -> oauth_redirect_url (with Set-Cookie: access/refresh/csrf)
Endpoints¶
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| GET | /auth/oauth/{provider}/authorize |
— | ?redirect_uri=... |
{url: str} |
| GET | /auth/oauth/{provider}/callback |
— | ?code=...&state=... |
TokenResponse (json) / 302 (cookie) |
| GET | /auth/oauth/accounts |
yes | — | [{provider, provider_account_id}] |
| DELETE | /auth/oauth/accounts/{provider} |
yes | — | {message} |
| GET | /auth/oauth/{provider}/link |
yes | ?redirect_uri=... |
{url} (409 if already linked) |
| GET | /auth/oauth/{provider}/link/callback |
(state-bound) | ?code=...&state=... |
{message} |
{provider} is google or github, matching provider.id.
Sign-in vs link¶
- Sign-in (
/authorize,/callback): unauthenticated flow. The state payload hasflow="signin". On callback, FastAuth looks up the linked OAuth account, then the user by email. If no user exists with that email, a new one is created withemail_verified=provider_email_verified. If a user exists but the provider saysemail_verified=False, the sign-in is rejected withProviderError("OAuth provider email is not verified")to prevent account takeover via an unverified provider email. - Link (
/link,/link/callback): authenticated flow. The state payload includesflow="link"and the caller'suser_id./linkrequires a Bearer access token;/link/callbackis not behindrequire_auth— it is state-bound to theuser_idstored in the state. Attempting to link an already-linked provider account returns400.
allow_signin hook¶
complete_oauth_flow calls hooks.allow_signin(user, provider_id) when it has a candidate user (newly created or matched by provider/email). Returning False → 403 Sign in not allowed.
Hooks fired¶
- New user:
on_signup, then (ifemail_verified=True)on_email_verify, thenon_signin. - Existing user:
on_signinonly. - Link success:
on_oauth_link.
Provider tokens¶
store_oauth_provider_tokens=True persists the provider's access_token and refresh_token in OAuthAccountData. Off by default; only enable if you need to call provider APIs on the user's behalf. OAuthAccountData.expires_at is currently not written by the link/sign-in flows (needs maintainer clarification on whether provider token expiry is tracked).
Common mistakes¶
400 oauth_state_store is not configured— you forgotoauth_state_store. Production needsRedisSessionBackend(or any sharedSessionBackend), notMemorySessionBackend, when running multiple workers.400 oauth_adapter is not configured— you forgotoauth_adapter=adapter.oauth.redirect_urinot inoauth_allowed_redirect_uris— once you set the allowlist, only exact matches are accepted. The provider callback is/auth/oauth/{provider}/callback— register that exact URL with Google/GitHub.- Setting
oauth_redirect_urlto the provider callback — it is your frontend URL, not the provider callback. - Multi-worker +
MemorySessionBackend— the state written on worker A is invisible to worker B →400 Invalid OAuth state. Use Redis.