Verifying user sessions from a Python backend, service to service
A FastAPI dependency that validates the session, hydrates the user, and keeps one pooled connection — plus the timeout budget an auth check should never exceed.
A Python service that accepts a user’s session and needs to know who they are has two Infrai reads available: GET /v1/auth/session/verify/{session_id} for the live session record and GET /v1/auth/user/get/{user_id} for the profile. Both are free per call and both are plain GETs, so the whole integration is requests or httpx with a pooled session and a strict timeout.
The engineering question isn’t how to call them. It’s how not to make your own API’s latency depend on someone else’s.
The dependency, end to end
import os
from dataclasses import dataclass
import httpx
from fastapi import Depends, FastAPI, Header, HTTPException
API = os.environ.get("INFRAI_API_BASE", "https://api.infrai.cc")
KEY = os.environ["INFRAI_API_KEY"]
# One client for the process. Creating an httpx.Client per request throws away
# connection reuse and TLS handshakes, which on a chatty auth check is most of
# the latency you'll measure.
client = httpx.Client(
base_url=API,
headers={"Authorization": f"Bearer {KEY}"},
timeout=httpx.Timeout(2.0, connect=1.0),
limits=httpx.Limits(max_keepalive_connections=20, max_connections=50),
)
app = FastAPI()
@dataclass(frozen=True)
class CurrentUser:
user_id: str
email: str
name: str | None
metadata: dict
def current_user(x_session_id: str = Header(...)) -> CurrentUser:
session = client.get(f"/v1/auth/session/verify/{x_session_id}")
if session.status_code == 404:
raise HTTPException(status_code=401, detail="session not found")
session.raise_for_status()
sdata = session.json()["data"]
profile = client.get(f"/v1/auth/user/get/{sdata['user_id']}")
profile.raise_for_status()
pdata = profile.json()["data"]
return CurrentUser(
user_id=pdata["user_id"],
email=pdata["email"],
name=pdata.get("name"),
metadata=pdata.get("metadata") or {},
)
@app.get("/me")
def me(user: CurrentUser = Depends(current_user)) -> dict:
return {"user_id": user.user_id, "email": user.email, "tenant": user.metadata.get("tenant_id")}
Two reads per request is honest but wasteful. The next section is how to get it down to zero in the common case.
Two calls per request is one too many
Most requests don’t need the live session record. They need to know the caller is who they say they are, and the access token already proves that — offline, against the published key set:
curl -sS "https://api.infrai.cc/v1/auth/token/jwks" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"keys": [
{"kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA", "kid": "infrai-auth-ed25519-v1",
"x": "KlELlwmJ87lR-5UPJ2JaagXal0Zo87THwPusrfKsKg4"}
]
}
}
Verify the EdDSA signature locally and you’ve spent microseconds. Keep the live read for the small set of routes where a revocation must bite immediately.
| Route class | What to do | Network cost |
|---|---|---|
| Ordinary reads | verify the JWT offline | none |
| Writes with side effects | offline verify + cached profile | none, warm |
| Destructive or financial | session/verify live | one round trip |
| Admin acting on another user | session/verify + user/get | two round trips |
Timeouts are an availability decision
An auth dependency with no timeout turns every one of your endpoints into a dependent of one HTTP call. Two seconds total with a one-second connect budget, as above, is a reasonable starting point for a check that normally returns in tens of milliseconds.
Then decide what happens when it doesn’t.
Failing closed — refusing the request — is correct for anything that mutates state. Failing open is occasionally right for a read-only, non-sensitive endpoint where showing stale public content beats showing an error page, but it must be a deliberate, documented choice for specific routes rather than a global fallback. The version where a timeout silently grants access is how an availability blip becomes an authorisation bug.
Caching the profile, not the decision
GET /v1/auth/user/get/{user_id} returns email, name, email_verified, mfa_enabled, created_at and your own metadata. That’s slow-changing data and a 60-second in-process cache is fine.
Don’t cache the verification result for the same window. The profile going stale for a minute is cosmetic; a revoked session staying valid for a minute is the thing you were trying to prevent.
curl -sS "https://api.infrai.cc/v1/auth/user/get/au_usr_lMmXGySJeGVM1xicqHGJaJbB" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
What this surface doesn’t give a Python service
The limitation is that there’s no Python SDK, no typed client and no framework integration: you write the dependency above, which is fine at 40 lines and less fine if you wanted middleware for five frameworks handed to you. SuperTokens ships framework-level integrations including Python, and if you want the recipe rather than the endpoints, it’s the better pick. Auth0’s Python quickstarts are similarly further along.
There’s also no push notification when a session dies, so a long-lived worker holding a user context learns about a revocation only when it next checks.
The compensating argument is what else that one client reaches. The same pooled httpx.Client and the same key can call POST /v1/errors/capture when a verification fails unexpectedly, POST /v1/metrics/report for the auth-latency histogram you’ll want, and POST /v1/logs/ingest for the audit line — no second vendor, no second credential in your config, one number in GET /v1/account/usage.
Both auth reads here report billing_class: free in discovery and aren’t billed per call; identity is metered per monthly active user, as with Auth0. Read your accrued figure live from GET /v1/account/usage (verified 2026-09-21) rather than trusting a number written down anywhere, and expect platform rates to fall over time.