Skip to main content

FastAPI Quickstart

Wire LumoAuth into a FastAPI app using the official lumoauth.fastapi router. One include_router(...) mounts /login, /callback, /logout; the get_current_user dependency is everything else.

Prerequisites​

  • Python 3.10+
  • An OAuth client registered with redirect URI http://localhost:8000/auth/callback.

Register the client in the portal under Applications → New application: enter an Application name, choose Web application, enter the Redirect URI, and click Create application. The client secret is shown once, on the page you land on.

The Create application form with Web application selected and a redirect URI entered

The Quickstarts page (under Applications & developer tools) has a FastAPI tab that shows the .env values for your organization and client pre-filled, ready to copy.

The Quickstarts page with the FastAPI tab selected, showing pre-filled environment variables

The tab's first step lists only OAuth clients of the kind these snippets need; if none exists, Create one for this framework opens the create form with the right type preselected and brings you back. Its last step, Check it worked, shows the first sign-in that completes through the selected client.

Or scaffold with the CLI:

lumo login --org your-org-slug
lumo init --framework fastapi --dir ./my-app
cd ./my-app && python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt && uvicorn main:app --reload --port 3000

Manual install​

pip install 'lumoauth[fastapi]' uvicorn[standard] python-dotenv itsdangerous

.env:

LUMO_BASE_URL=https://app.lumoauth.dev
LUMO_ORG_ID=your-org-slug
LUMO_CLIENT_ID=your-oauth-client-id
LUMO_CLIENT_SECRET=your-oauth-client-secret
SESSION_SECRET=change-me-to-a-random-32-byte-string

main.py:

import os
from dotenv import load_dotenv
from fastapi import FastAPI, Depends
from fastapi.responses import RedirectResponse, HTMLResponse
from starlette.middleware.sessions import SessionMiddleware
from lumoauth.fastapi import (
lumo_auth_router,
get_current_user,
require_auth,
User,
)

load_dotenv()

app = FastAPI()

# SessionMiddleware MUST be mounted before lumo_auth_router.
app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"])

app.include_router(
lumo_auth_router(
base_url=os.environ["LUMO_BASE_URL"],
organization=os.environ["LUMO_ORG_ID"],
client_id=os.environ["LUMO_CLIENT_ID"],
client_secret=os.environ.get("LUMO_CLIENT_SECRET"), # omit for public
callback_path="/auth/callback",
),
prefix="/auth",
)


@app.get("/", response_class=HTMLResponse)
def home(user: User | None = Depends(get_current_user)):
if user is None:
return RedirectResponse("/auth/login")
return f"<h1>Hi {user.email}</h1><a href='/auth/logout'>Sign out</a>"


@app.get("/api/me")
def me(user: User = Depends(require_auth())):
return user.model_dump()

Run with uvicorn main:app --reload, visit http://localhost:8000 — you'll be redirected to LumoAuth, sign in, and bounce back authenticated.

Optional vs required auth​

# Optional — works for both anonymous and authenticated callers.
@app.get("/dashboard")
def dashboard(user: User | None = Depends(get_current_user)):
if user is None:
return RedirectResponse("/auth/login")
return {"name": user.name}

# Required — 401 if no session.
@app.get("/api/me")
def me(user: User = Depends(require_auth())):
return user.model_dump()

# Required + scope check — 403 if any required scope is missing.
@app.get("/api/admin")
def admin(user: User = Depends(require_auth(scopes=["admin"]))):
return {"ok": True}

Reading custom claims​

The User model uses Pydantic v2 with extra="allow", so any custom claims your organization adds to /userinfo (org-specific traits, custom scopes, etc.) come through as attributes:

@app.get("/api/me")
def me(user: User = Depends(require_auth())):
return {
"email": user.email,
"name": user.name,
# Standard claims are typed
"department": getattr(user, "department", None),
# Custom claims fall through model_dump()
"all_claims": user.model_dump(),
}

Reference​