Skip to content

OAuth2 Social Login

Add Google and GitHub login with a few config lines. Users can link multiple providers alongside email/password login.

Installation

pip install fastapi-fullauth[oauth]

Configuration

from fastapi_fullauth import FullAuth, FullAuthConfig
from fastapi_fullauth.oauth.google import GoogleOAuthProvider
from fastapi_fullauth.oauth.github import GitHubOAuthProvider

fullauth = FullAuth(
    adapter=adapter,
    config=FullAuthConfig(SECRET_KEY="..."),
    providers=[
        GoogleOAuthProvider(
            client_id="your-google-client-id",
            client_secret="your-google-secret",
            redirect_uris=[
                "http://localhost:3000/auth/callback",
                "https://myapp.com/auth/callback",
            ],
        ),
        GitHubOAuthProvider(
            client_id="your-github-client-id",
            client_secret="your-github-secret",
            redirect_uris=["http://localhost:3000/auth/callback"],
        ),
    ],
)

Tip

redirect_uris is the list of allowed callback URLs. The client must pass redirect_uri as a query parameter in the authorize request = the library validates it against this list.

Routes

When OAuth providers are configured, these routes are registered automatically:

Method Path Description
GET /auth/oauth/providers List configured providers
GET /auth/oauth/{provider}/authorize Get authorization URL
POST /auth/oauth/{provider}/callback Exchange code for tokens
GET /auth/oauth/accounts List linked OAuth accounts
DELETE /auth/oauth/accounts/{provider} Unlink a provider

How the flow works

1. Get the authorization URL

GET /api/v1/auth/oauth/google/authorize?redirect_uri=http://localhost:3000/auth/callback

Response:

{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..."
}

The redirect_uri parameter is optional. If omitted, the first URI in your redirect_uris list is used. The value is validated against the allowed list.

2. Redirect the user

Your frontend redirects the user to the authorization_url. The user authenticates with Google/GitHub.

3. Handle the callback

The provider redirects back to your redirect_uri with code and state query parameters. Your frontend sends these to the callback endpoint:

POST /api/v1/auth/oauth/google/callback
{
  "code": "4/0AX4XfW...",
  "state": "eyJ..."
}

Response:

{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 1800,
  "user": { "id": "...", "email": "user@example.com", "is_active": true, "is_verified": true }
}

From this point on, the session works exactly like email/password login. The user can call /me, /refresh, /logout, etc. with the JWT tokens.

What happens on callback

  1. State token is verified (CSRF protection, 5-minute TTL)
  2. Authorization code is exchanged for provider tokens
  3. User info is fetched from the provider (email, name, picture)
  4. Account linking logic runs:
    • If this provider account is already linked - update tokens, return existing user
    • If an account with the same email exists and the provider confirms the email is verified - link the OAuth account to it
    • If the email exists but the provider hasn't verified it - reject the login (security check)
    • Otherwise - create a new user (marked as verified, since the provider confirmed their email)
  5. JWT tokens are issued (same as regular login)

Warning

Auto-linking only happens when the provider reports email_verified=True. This prevents an attacker from creating a provider account with a victim's email and hijacking their local account. If the provider hasn't verified the email, the user must log in with their existing credentials and link the provider manually.

Auto-linking by email

By default, if a user registers with user@example.com via email/password, then later logs in with Google using the same verified email, the accounts are linked automatically. Disable this with:

config = FullAuthConfig(
    SECRET_KEY="...",
    OAUTH_AUTO_LINK_BY_EMAIL=False,
)

Security model

State token: the OAuth state parameter is a purpose-scoped JWT with a 5-minute TTL (OAUTH_STATE_EXPIRE_SECONDS). It prevents CSRF attacks on the callback endpoint. If the state token is missing, expired, or tampered with, the callback is rejected.

Redirect URI validation: the library validates the redirect_uri parameter against the provider's configured redirect_uris list. Mismatched URIs are rejected with a 400 error.

Token storage: provider access and refresh tokens are stored in the oauth_accounts table and updated on each login.

OAuth-only users

Users created through OAuth login have no password hash. They authenticate exclusively through their linked provider.

  • To set a password later, use POST /change-password. The current_password field is not required for users without an existing password.
  • Unlinking a provider is blocked if it's the user's only login method (no password set, no other linked providers). The user must set a password first.

Unlinking providers

Users can unlink an OAuth provider:

DELETE /api/v1/auth/oauth/accounts/google

This is blocked if the OAuth account is the user's only login method (no password set, no other OAuth providers). The user must set a password first.

Adding your own provider

Subclass OAuthProvider and implement three methods:

from fastapi_fullauth.oauth.base import OAuthProvider, OAuthUserInfo

class MyProvider(OAuthProvider):
    name = "myprovider"

    @property
    def default_scopes(self) -> list[str]:
        return ["openid", "email"]

    def get_authorization_url(self, state: str, redirect_uri: str) -> str:
        # build and return the provider's authorization URL
        ...

    async def exchange_code(self, code: str, redirect_uri: str) -> dict:
        # POST the code to the provider's token endpoint
        # return {"access_token": "...", "refresh_token": "...", ...}
        ...

    async def get_user_info(self, tokens: dict) -> OAuthUserInfo:
        # fetch user info from the provider using the access token
        return OAuthUserInfo(
            provider=self.name,
            provider_user_id="...",
            email="user@example.com",
            email_verified=True,
            name="User Name",
            picture=None,
            raw={},  # full provider response
        )

See GoogleOAuthProvider and GitHubOAuthProvider in the source for complete examples.

Event hooks

Two hooks fire during OAuth flows:

  • after_oauth_login fires on every OAuth login (new and returning users):
async def on_oauth_login(user, provider, is_new_user):
    if is_new_user:
        print(f"New user via {provider}: {user.email}")

fullauth.hooks.on("after_oauth_login", on_oauth_login)
  • after_oauth_register fires only when a new user is created via OAuth, and includes the provider's user info:
async def on_oauth_register(user, user_info):
    print(f"New OAuth user: {user.email}, provider data: {user_info.raw}")

fullauth.hooks.on("after_oauth_register", on_oauth_register)

The after_register hook also fires for new OAuth users.

Provider setup guides

Google

  1. Go to Google Cloud Console
  2. Create a project (or select existing)
  3. Go to APIs & Services > Credentials
  4. Create an OAuth 2.0 Client ID (Web application)
  5. Add your redirect URIs under Authorized redirect URIs
  6. Copy the Client ID and Client Secret

Default scopes: openid, email, profile

GitHub

  1. Go to GitHub Developer Settings
  2. Click New OAuth App
  3. Set the Authorization callback URL to your redirect URI
  4. Copy the Client ID and Client Secret

Default scopes: read:user, user:email

Note

GitHub requires a separate API call to fetch the user's verified primary email. The library handles this automatically.