Skip to content

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_store stores OAuth CSRF + PKCE state (TTL 600s). Use RedisSessionBackend in production so multi-process deployments share state.
  • oauth_redirect_url is the frontend URL FastAuth 302s to after a successful callback. The access/refresh tokens are set as HttpOnly cookies on that response, never appended to the URL.
  • oauth_allowed_redirect_uris is the allowlist of exact provider-callback URLs the user may put in redirect_uri on 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 (/authorize, /callback): unauthenticated flow. The state payload has flow="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 with email_verified=provider_email_verified. If a user exists but the provider says email_verified=False, the sign-in is rejected with ProviderError("OAuth provider email is not verified") to prevent account takeover via an unverified provider email.
  • Link (/link, /link/callback): authenticated flow. The state payload includes flow="link" and the caller's user_id. /link requires a Bearer access token; /link/callback is not behind require_auth — it is state-bound to the user_id stored in the state. Attempting to link an already-linked provider account returns 400.

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 False403 Sign in not allowed.

Hooks fired

  • New user: on_signup, then (if email_verified=True) on_email_verify, then on_signin.
  • Existing user: on_signin only.
  • 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 forgot oauth_state_store. Production needs RedisSessionBackend (or any shared SessionBackend), not MemorySessionBackend, when running multiple workers.
  • 400 oauth_adapter is not configured — you forgot oauth_adapter=adapter.oauth.
  • redirect_uri not in oauth_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_url to 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.