fastapi-sso

repository·master·Indexed 19 days ago

https://github.com/tomasvotava/fastapi-sso

A FastAPI plugin to enable Single Sign-On (SSO) integration with common providers such as Google, Facebook, Microsoft, Github, and others. It supports official providers including Spotify, Fitbit, Notion, and Twitter (X), as well as community-contributed providers like Discord, Bitbucket, and Apple. The library also provides a generic OIDC provider implementation via create_provider for custom OpenID Connect integrations. Version 0.21.1.

Tokens
13.3K
Snippets
29
Records
35
Agent score
66%

What's inside fastapi-sso

  1. What is the `state` parameter and how should it be used?

    master

    The state parameter is a security mechanism designed to protect against CSRF attacks. It allows the client to verify that an authorization response belongs to a request it previously initiated.

    Key Rules for state:

    • Purpose: Verification of the authentication request, not generic data transport.
    • Security Requirement: To be secure, a state value must be cryptographically random, stored server-side, and verified upon redirect.
    • Automatic Handling: If you do not pass a state explicitly, fastapi-sso automatically generates, stores, and validates a secure random state for you.
    • Data Transport: If you need to preserve data (like a return_url) across the login flow, do not use state for this. Instead, store the data in a server-side session and let fastapi-sso manage the state for security.
  2. How to use the `state` parameter for context

    master

    The state parameter is a security mechanism designed to protect against CSRF attacks. fastapi-sso automatically generates, stores, and validates a secure random state if you do not provide one.

    Security Warning: Do not use state to carry arbitrary, unvalidated user-controlled data (like return URLs) as this is unsafe. The recommended way to preserve context (e.g., a return URL) is to store that data in a server-side session and use state only for request verification.

    If you must use state to pass a return URL (for compatibility or specific patterns), you can pass it to get_login_redirect and retrieve it from the callback function.

    from fastapi import Request
    from fastapi.responses import RedirectResponse
    
    google_sso = GoogleSSO("client-id", "client-secret")
    
    # E.g. https://example.com/auth/login?return_url=https://example.com/welcome
    async def google_login(return_url: str):
        # Send return_url to Google as a state so that Google knows to return it back to us
        async with google_sso:
            return await google_sso.get_login_redirect(redirect_uri=request.url_for("google_callback"), state=return_url)
    
    async def google_callback(request: Request, state: str | None = None):
        async with google_sso:
            user = await google_sso.verify_and_process(request)
            if state is not None:
                return RedirectResponse(state)
            else:
                return user
  3. Handle missing user data by requesting additional scopes

    master

    Some SSO providers may return incomplete data or change their response format. If you encounter KeyError or missing fields (like an email address), you can request additional permissions using the scope parameter.

    For example, when using Microsoft SSO, you might need the User.Read.All or email scope to access the user's email address. Note that email was added as a default scope for Microsoft SSO in version 0.8.0.

  4. Run FastAPI-SSO examples

    master

    To run the provided examples (such as the Google SSO example), you must provide your OAuth credentials as environment variables. Set CLIENT_ID and CLIENT_SECRET before executing the Python script.

    CLIENT_ID="client-id" CLIENT_SECRET="client-secret" python examples/google.py
  5. How to use `state` for context preservation (Unsafe Pattern)

    master

    The state parameter in fastapi-sso can be used to carry contextual data (like a return_url) through the OAuth flow. When you pass a value to the state argument in get_login_redirect, the provider (e.g., Google) will return that exact value to your callback. You can then access this value via the state parameter in your callback function or via the .state property.

    ⚠️ SECURITY WARNING: Using state to carry arbitrary user-controlled data without validation is unsafe and can lead to CSRF vulnerabilities. The recommended secure approach is to store contextual data (like a return URL) in a server-side session and use state exclusively for its intended purpose: cryptographically random request verification. fastapi-sso handles secure state generation and validation automatically if you do not provide a state explicitly.

    from fastapi import Request
    from fastapi.responses import RedirectResponse
    from fastapi_sso import GoogleSSO
    
    google_sso = GoogleSSO("client-id", "client-secret")
    
    # E.g. https://example.com/auth/login?return_url=https://example.com/welcome
    async def google_login(request: Request, return_url: str):
        async with google_sso:
            # Send return_url to Google as a state so that Google knows to return it back to us
            return await google_sso.get_login_redirect(
                redirect_uri=request.url_for("google_callback"), 
                state=return_url
            )
    
    async def google_callback(request: Request, state: str | None = None):
        async with google_sso:
            user = await google_sso.verify_and_process(request)
            if state is not None:
                # Redirect to the URL stored in the state
                return RedirectResponse(state)
            else:
                return user
  6. Configure insecure HTTP for local development

    master

    When testing on localhost without using HTTPS (self-signed certificates), you must configure both your SSO provider and the fastapi-sso library to allow insecure transport.

    1. SSO Provider Configuration: Set your redirect URI within your SSO provider (e.g., Google Console) to http://localhost:{port}.
    2. Library Configuration: Pass allow_insecure_http=True to the constructor of your specific SSO class (e.g., GoogleSSO).

    Note on OAUTHLIB_INSECURE_TRANSPORT: Since version 0.9.0, setting allow_insecure_http=True automatically handles the OAUTHLIB_INSECURE_TRANSPORT=1 environment variable requirement, so you no longer need to set it manually in your environment. However, the example below shows the manual way for completeness.

    Warning: Always use https in production environments.

    import os
    from fastapi_sso.sso.google import GoogleSSO
    
    # Note: Since 0.9.0, this env var is set automatically if allow_insecure_http=True
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
    
    google_sso = GoogleSSO("client-id", "client-secret", allow_insecure_http=True)
  7. Implement Google SSO with a minimal example

    master

    To implement Google SSO, you must first create a Google OAuth2 client in the Google Cloud Platform Console.

    1. Create a project and a new OAuth2 client.
    2. Set the Authorized redirect URIs to your callback URL (e.g., http://localhost:3000/google/callback).
    3. Copy the Client ID and Client secret.

    Use the GoogleSSO class to handle the login redirect and the callback verification. Note that GoogleSSO should be used as an asynchronous context manager (async with) to ensure proper state handling.

    from fastapi import FastAPI
    from starlette.requests import Request
    from fastapi_sso.sso.google import GoogleSSO
    
    app = FastAPI()
    
    CLIENT_ID = "your-google-client-id"  # <-- paste your client id here
    CLIENT_SECRET = "your-google-client-secret" # <-- paste your client secret here
    
    google_sso = GoogleSSO(CLIENT_ID, CLIENT_SECRET, "http://localhost:3000/google/callback")
    
    @app.get("/google/login")
    async def google_login():
        async with google_sso:
            return await google_sso.get_login_redirect()
    
    @app.get("/google/callback")
    async def google_callback(request: Request):
        async with google_sso:
            user = await google_sso.verify_and_process(request)
        return user
  8. Integrate fastapi-sso with FastAPI Security

    master

    Since fastapi-sso handles the communication with the login provider but does not manage session state, you must implement your own authentication mechanism (e.g., using JWTs) to protect endpoints.

    To add the lock 🔒 icon to your Swagger/OpenAPI documentation and enforce authentication via cookies, use FastAPI's Security and APIKeyCookie classes.

    Requirements

    • fastapi
    • fastapi-sso
    • python-jose[cryptography] (to sign and verify JWTs)

    Implementation Pattern

    1. Login: Use sso.get_login_redirect() to send users to the provider.
    2. Callback: Use sso.verify_and_process(request) to get the OpenID object, then generate a signed JWT and store it in a cookie.
    3. Protection: Create a dependency function using Security(APIKeyCookie(name="token")) that decodes the JWT and returns the user data. Use this dependency in your protected routes via Depends().
    import datetime
    from fastapi import FastAPI, Depends, HTTPException, Security, Request
    from fastapi.responses import RedirectResponse
    from fastapi.security import APIKeyCookie
    from fastapi_sso.sso.google import GoogleSSO
    from fastapi_sso.sso.base import OpenID
    from jose import jwt
    
    SECRET_KEY = "this-is-very-secret"
    CLIENT_ID = "your-client-id"
    CLIENT_SECRET = "your-client-secret"
    
    sso = GoogleSSO(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri="http://127.0.0.1:5000/auth/callback")
    app = FastAPI()
    
    async def get_logged_user(cookie: str = Security(APIKeyCookie(name="token"))) -> OpenID:
        """Get user's JWT stored in cookie 'token', parse it and return the user's OpenID."""
        try:
            claims = jwt.decode(cookie, key=SECRET_KEY, algorithms=["HS256"])
            return OpenID(**claims["pld"])
        except Exception as error:
            raise HTTPException(status_code=401, detail="Invalid authentication credentials") from error
    
    @app.get("/protected")
    async def protected_endpoint(user: OpenID = Depends(get_logged_user)):
        return {"message": f"You are very welcome, {user.email}!"}
    
    @app.get("/auth/login")
    async def login():
        async with sso:
            return await sso.get_login_redirect()
    
    @app.get("/auth/logout")
    async def logout():
        response = RedirectResponse(url="/protected")
        response.delete_cookie(key="token")
        return response
    
    @app.get("/auth/callback")
    async def login_callback(request: Request):
        async with sso:
            openid = await sso.verify_and_process(request)
            if not openid:
                raise HTTPException(status_code=401, detail="Authentication failed")
        
        expiration = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1)
        token = jwt.encode({"pld": openid.dict(), "exp": expiration, "sub": openid.id}, key=SECRET_KEY, algorithm="HS256")
        response = RedirectResponse(url="/protected")
        response.set_cookie(key="token", value=token, expires=expiration)
        return response
  9. Use the SSO instance with an async context manager

    master

    To prevent race conditions and ensure proper handling of asynchronous operations (especially in high-concurrency environments), you must use the SSO instance within an async with context manager when processing requests. Using the synchronous with context manager is deprecated and will trigger a warning.

    # Recommended approach
    async with sso:
        openid = await sso.verify_and_process(request)