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:
| Option | Console value | Example |
|---|---|---|
tenant_id | Project ID | ten_abcdefghijklmnop |
issuer | Issuer | https://auth.auth4.dev/t/ten_abcdefghijklmnop |
audience | Audience (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:
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"])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
401withWWW-Authenticate: Bearer realm="auth4". any_rolerequires at least one of the listed roles,allow_guests=Falselets only upgraded players through, andscopesrequires granted scopes. A valid token that fails one of these gets403.- 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:
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:
examples/python/django_app.pyAUTH4={
"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:
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/profileWithout a framework
In any other Python backend, use Auth4ServerClient directly. It is synchronous and safe to share between threads:
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
| Attribute | Claim | Meaning |
|---|---|---|
subject | sub | The player's user ID. It stays the same when a guest upgrades. |
tenant_id | tenant_id | Your project ID. Always equal to the configured tenant_id. |
client_id | client_id | The application the token was issued to. |
scopes | scope | Granted scopes. Check one with has_scope(). |
session_id | sid | The session ID, when the token belongs to a session. |
is_guest | auth4_guest | True while the player hasn't upgraded their guest account. The next token after an upgrade says False. |
roles | auth4_roles | The player's roles in this application. Check them with has_role() orhas_any_role(). See Roles and metadata. |
metadata | auth4_meta | The player's flat metadata, as a read-only mapping. Empty when the player has none. |
expires_at | exp | At 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 type
at+jwt. iss,aud,tenant_id,expandnbf, 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 codeinvalid_tokenmeans the token is bad.jwks_unavailable,jwks_refresh_limitedandintrospection_unavailableare outages, andis_outageisTrue. - The FastAPI and Django integrations answer an outage with
503andRetry-After: 5instead of401, 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:
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:
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 as
introspection_url. - An active answer is reused for
cache_ttlseconds: 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 with
introspection_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.