Python server quickstart

The Python server SDK verifies auth4.dev access tokens the same way as the TypeScript server SDK. Use it in a FastAPI or Django API, or any other Python game backend.

Before you start

You need three values from the console:

OptionConsole valueExample
tenant_idProject IDten_abcdefghijklmnop
issuerIssuerhttps://auth.auth4.dev/t/ten_abcdefghijklmnop
audienceAudience (the client ID of the application calling your API)cli_…

Instead of issuer you can pass auth_origin, for examplehttps://auth.auth4.dev, and the SDK uses{auth_origin}/t/{tenant_id}. Signing keys always come from {issuer}/.well-known/jwks.json, never from a URL in the token.

1. Add the SDK

The SDK is the auth4 package in packages/sdk-python. It needs Python 3.10 or later and depends only on cryptography. The fastapi anddjango extras add the framework integrations. It isn't on PyPI yet, so install it from a copy of the repository:

pip install -e "path/to/auth4/packages/sdk-python[fastapi]"   # or [django]

2. Protect a FastAPI API

Auth4Bearer is a dependency that returns the verified token. Create one per rule and reuse it:

Excerpt from examples/python/fastapi_app.py
# One client per audience. Reuse it, so its signing-key cache is shared by every request.
auth4 = Auth4ServerClient(tenant_id=TENANT_ID, auth_origin=AUTH_ORIGIN, audience=AUDIENCE)

player = Auth4Bearer(auth4)
moderator = Auth4Bearer(auth4, any_role=["moderator"])
Excerpt from examples/python/fastapi_app.py
@app.get("/profile")
def profile(token: AccessToken = Depends(player)) -> dict[str, object]:
    # token.subject is the player's user ID. It stays the same when a guest upgrades.
    return {
        "player": token.subject,
        "guest": token.is_guest,
        "roles": list(token.roles),
        "level": token.metadata.get("level"),
    }


@app.post("/moderation/kick")
def kick(token: AccessToken = Depends(moderator)) -> dict[str, object]:
    return {"kicked_by": token.subject}


@app.post("/trade")
def trade(token: AccessToken = Depends(upgraded)) -> dict[str, object]:
    # Guests get 403. With introspect mode, a revoked session is refused within five seconds
    # instead of when its five-minute access token expires.
    return {"trader": token.subject}
  • A missing or invalid token gets 401 with WWW-Authenticate: Bearer realm="auth4".
  • any_role requires at least one of the listed roles, allow_guests=False lets only upgraded players through, and scopes requires granted scopes. A valid token that fails one of these gets 403.
  • The dependency is a plain function, so FastAPI runs it in its threadpool and a key fetch never blocks the event loop.

To catch a wrong project ID or auth origin at startup, call discover(). It checks that the issuer's discovery document names the same issuer and key URL:

Excerpt from examples/python/fastapi_app.py
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    # Fail fast on a wrong project ID or auth origin. Verification itself never needs discovery.
    auth4.discover()
    yield


app = FastAPI(title="auth4.dev Python example", lifespan=lifespan)

Protect a Django API

Configure the client once with the AUTH4 setting. The single-file example passes it tosettings.configure(); in a settings.py, writeAUTH4 = {…} with the same keys:

Excerpt from examples/python/django_app.py
AUTH4={
    "TENANT_ID": os.environ["AUTH4_TENANT_ID"],
    "AUTH_ORIGIN": os.environ.get("AUTH4_AUTH_ORIGIN", "https://auth.auth4.dev"),
    "AUDIENCE": os.environ["AUTH4_AUDIENCE"],
},

Then decorate views with auth4_required(). It works on sync and async views and puts the verified token on request.auth4:

Excerpt from examples/python/django_app.py
@require_GET
@auth4_required()
def profile(request: HttpRequest) -> JsonResponse:
    token = request.auth4  # type: ignore[attr-defined]
    return JsonResponse({"player": token.subject, "guest": token.is_guest})


@require_POST
@auth4_required(any_role=["moderator"])
def kick(request: HttpRequest) -> JsonResponse:
    return JsonResponse({"kicked_by": request.auth4.subject})  # type: ignore[attr-defined]


# Django 4.2's require_POST doesn't wrap async views, so this one checks the method itself.
@auth4_required(allow_guests=False)
async def trade(request: HttpRequest) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])
    return JsonResponse({"trader": request.auth4.subject})  # type: ignore[attr-defined]

Refusals are JSON: 401{"error": "unauthorized"}, 403{"error": "forbidden"} and, for outages, 503{"error": "unavailable"}. For Django REST framework or your own checks, authenticate() verifies a request and get_client() returns the shared client.

Run either example from the repository root:

export AUTH4_TENANT_ID=ten_… AUTH4_AUTH_ORIGIN=https://auth.auth4.dev AUTH4_AUDIENCE=cli_…
uvicorn --app-dir examples/python fastapi_app:app --port 8000
python examples/python/django_app.py runserver 8001

curl -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8000/profile

Without a framework

In any other Python backend, use Auth4ServerClient directly. It is synchronous and safe to share between threads:

Verify a request
from auth4 import Auth4ServerClient, Auth4TokenError

# One client per audience. Reuse it, so its signing-key cache is shared.
auth4 = Auth4ServerClient(
    tenant_id="ten_…",
    auth_origin="https://auth.auth4.dev",  # or issuer="https://auth.auth4.dev/t/ten_…"
    audience="cli_…",
)

try:
    token = auth4.validate_authorization_header(authorization_header)
    # token.subject is the player's user ID. Key your game data on it.
except Auth4TokenError as error:
    if error.is_outage:
        ...  # Signing keys or introspection were unavailable: report an outage, don't sign out.
    else:
        ...  # Missing, malformed, expired or otherwise invalid: reject the request.

3. Use the claims

AttributeClaimMeaning
subjectsubThe player's user ID. It stays the same when a guest upgrades.
tenant_idtenant_idYour project ID. Always equal to the configured tenant_id.
client_idclient_idThe application the token was issued to.
scopesscopeGranted scopes. Check one with has_scope().
session_idsidThe session ID, when the token belongs to a session.
is_guestauth4_guestTrue while the player hasn't upgraded their guest account. The next token after an upgrade says False.
rolesauth4_rolesThe player's roles in this application. Check them with has_role() orhas_any_role(). See Roles and metadata.
metadataauth4_metaThe player's flat metadata, as a read-only mapping. Empty when the player has none.
expires_atexpAt most five minutes after issue, in epoch seconds.

What the SDK checks

  • The RS256 signature, with a key from the configured issuer, and the token typeat+jwt.
  • iss, aud, tenant_id, exp andnbf, with no clock tolerance by default. Allow up to 60 seconds of drift withclock_tolerance.
  • The claim rules auth4.dev issues tokens with. A token with an unknown or repeated claim, a malformed role or a nested metadata value is rejected.
  • ID tokens, tokens from other projects and console management tokens are always rejected.

ID tokens have their own method, validate_id_token(). Use it only to confirm a sign-in for your own client ID, never to authorize an API call:

id_token = auth4.validate_id_token(raw_id_token, client_id="cli_…", expected_nonce=nonce)

Failures and key rotation

  • Signing keys are cached for five minutes. A token signed with an unknown key ID triggers one key refresh, at most once every 30 seconds, so key rotation needs no restart. Concurrent requests share one fetch.
  • Failures raise Auth4TokenError. Only the code invalid_token means the token is bad. jwks_unavailable, jwks_refresh_limited andintrospection_unavailable are outages, and is_outage isTrue.
  • The FastAPI and Django integrations answer an outage with 503 and Retry-After: 5instead of 401, so players aren't signed out.

Introspect mode

Introspect mode asks the project's introspection endpoint whether the token's session is still live after offline verification passes. It needs the client ID and secret of a confidential server client of the same project, passed as IntrospectionOptions:

Introspect mode
from auth4 import IntrospectionOptions

sensitive = Auth4ServerClient(
    tenant_id="ten_…",
    auth_origin="https://auth.auth4.dev",
    audience="cli_…",
    introspection=IntrospectionOptions(
        client_id=os.environ["AUTH4_SERVER_CLIENT_ID"],
        client_secret=os.environ["AUTH4_SERVER_CLIENT_SECRET"],
        cache_ttl=5,
    ),
)

The FastAPI example uses a second client for sensitive endpoints only, so other endpoints don't pay for the extra request:

Excerpt from examples/python/fastapi_app.py
# Introspect mode for sensitive endpoints, when server client credentials are configured.
# Read the secret from your secret store or environment; never commit it.
server_client_id = os.environ.get("AUTH4_SERVER_CLIENT_ID")
server_client_secret = os.environ.get("AUTH4_SERVER_CLIENT_SECRET")
sensitive = (
    Auth4ServerClient(
        tenant_id=TENANT_ID,
        auth_origin=AUTH_ORIGIN,
        audience=AUDIENCE,
        introspection=IntrospectionOptions(
            client_id=server_client_id, client_secret=server_client_secret, cache_ttl=5
        ),
    )
    if server_client_id and server_client_secret
    else auth4
)
upgraded = Auth4Bearer(sensitive, allow_guests=False)

In Django, add the credentials to the settings:

AUTH4 = {
    # …
    "INTROSPECTION": {
        "CLIENT_ID": os.environ["AUTH4_SERVER_CLIENT_ID"],
        "CLIENT_SECRET": os.environ["AUTH4_SERVER_CLIENT_SECRET"],
    },
}
  • The endpoint is derived from the configured issuer, never from the token. It is available asintrospection_url.
  • An active answer is reused for cache_ttl seconds: 5 by default, at most 60, and zero to check every request. An inactive answer is kept until the token expires.
  • If the endpoint can't be reached or answers with an error, verification fails closed withintrospection_unavailable.
  • The SDK never logs the secret or includes it in repr(). Load it from your secret store or environment, never from source code.

Create a server client in the console under Applications; see Server clients and their secrets.

SDK notes: packages/sdk-python/README.md. Examples: examples/python/README.md. Tests: packages/sdk-python/tests and tests/integration/auth4d-171.python-sdk.test.ts.