FastAPI setup
This guide shows an example of setting up a FastAPI app with Zitadel authentication.
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Security, Depends
from pydantic import HttpUrl
from fastapi_zitadel_auth import ZitadelAuth
from fastapi_zitadel_auth.user import DefaultZitadelUser
from fastapi_zitadel_auth.exceptions import ForbiddenException
# IDs from Zitadel console
CLIENT_ID = 'your-zitadel-client-id'
PROJECT_ID = 'your-zitadel-project-id'
# Create a ZitadelAuth object usable as a FastAPI dependency
zitadel_auth = ZitadelAuth(
issuer_url=HttpUrl('https://your-instance-xyz.zitadel.cloud'),
project_id=PROJECT_ID,
app_client_id=CLIENT_ID,
allowed_scopes={
"openid": "OpenID Connect",
"email": "Email",
"profile": "Profile",
"urn:zitadel:iam:org:project:id:zitadel:aud": "Audience",
"urn:zitadel:iam:org:projects:roles": "Roles",
}
)
Audience and client binding
A token is accepted only if both:
- its
audclaim contains the configuredapp_client_id, and - its
client_idclaim equals the configuredapp_client_id.
By default, Zitadel includes every app in a project — plus the project ID — in the access token's aud claim, so audience matching alone accepts tokens issued for sibling apps in the same project. The client_id claim is the only reliable per-app discriminator. See Zitadel's audience validation guidance.
Clock skew (token_leeway)
The optional token_leeway parameter (seconds) is applied to the exp / nbf / iat checks. Default 0; the library hard-caps it at 30 seconds and raises ValueError for higher values. RFC 7519 §4.1.4/§4.1.5 permits "some small leeway, usually no more than a few minutes", but a tighter cap preserves the value of exp for short-lived OAuth2 access tokens. Synchronise host clocks via NTP rather than widening this window.
# Create a dependency to validate that the user has the required role
async def validate_is_admin_user(user: DefaultZitadelUser = Depends(zitadel_auth)) -> None:
required_role = "admin"
if required_role not in user.claims.project_roles.keys():
raise ForbiddenException(f"User does not have role assigned: {required_role}")
# Load OpenID configuration at startup
@asynccontextmanager
async def lifespan(app: FastAPI): # noqa
await zitadel_auth.openid_config.load_config()
yield
# Create a FastAPI app and configure Swagger UI
app = FastAPI(
title="fastapi-zitadel-auth demo",
lifespan=lifespan,
swagger_ui_oauth2_redirect_url="/oauth2-redirect",
swagger_ui_init_oauth={
"usePkceWithAuthorizationCodeGrant": True,
"clientId": CLIENT_ID,
"scopes": " ".join( # defining the pre-selected scope ticks in the Swagger UI
[
"openid",
"profile",
"email",
"urn:zitadel:iam:org:projects:roles",
"urn:zitadel:iam:org:project:id:zitadel:aud",
]
),
},
)
# Endpoint that requires a user to be authenticated and have the admin role
@app.get(
"/api/protected/admin",
summary="Protected endpoint, requires admin role",
dependencies=[Security(validate_is_admin_user)],
)
def protected_for_admin(request: Request):
user = request.state.user
return {"message": "Hello world!", "user": user}
# Endpoint that requires a user to be authenticated and have a specific scope
@app.get(
"/api/protected/scope",
summary="Protected endpoint, requires a specific scope",
dependencies=[Security(zitadel_auth, scopes=["scope1"])],
)
def protected_by_scope(request: Request):
user = request.state.user
return {"message": "Hello world!", "user": user}
Customizing OpenAPI documentation
You can optionally customize how the authentication scheme appears in Swagger UI:
zitadel_auth = ZitadelAuth(
...,
scheme_name="ZitadelAuth", # Optional (default: "ZitadelAuthorizationCodeBearer")
description="OAuth2 authentication via Zitadel", # Optional
)
Optional parameters
Both scheme_name and description are optional and have sensible defaults. Only customize them if you want to change how the authentication scheme appears in your API documentation.
CORS Middleware
For production you may need to add a CORS middleware to your FastAPI app.