Short answer: model global logout as three observable transitions: enumerate a user's sessions, revoke all of them, then enumerate again and require the documented empty-state result before reporting success.
For a FastAPI healthtech service that scores login risk from device fingerprints, the important decision isn't which logout button looks easiest to wire. It is whether migration away from a managed authentication provider preserves a traceable user-to-session relationship and gives the eval harness evidence that every session changed state. A successful POST is an action result; it isn't proof of the postcondition.
That distinction is the whole experiment.
How should a global logout workflow enumerate sessions, revoke all, and verify the result?
Treat session creation, validation, refresh, and revocation as separate lifecycle actions. Access credentials should be short-lived, while refresh capability gets a different risk policy because it can extend access. Current-device logout revokes one session. Global logout targets every session linked to the user. Those are different product promises and should never share an ambiguous logout() operation.
The tempting first pass is: call revoke-all, accept any 2xx response, and redirect the patient or clinician to sign-in. That is simple, but the test observes only the command. The stronger workflow captures the session set before the command, performs an idempotent revocation, and then asks the same authoritative list operation for the state afterward. Keep both snapshots with the request ID from your own application boundary so an audit can answer who initiated the transition, which user was targeted, and what the verifier observed. Don't place raw device fingerprints or bearer credentials in that record.
There is a subtle risk-scoring consequence. A device fingerprint can inform whether a login deserves step-up checks, but logout state still belongs to the session system; otherwise a changed fingerprint can accidentally become a substitute for revocation. Keep the risk decision, access-credential lifetime, refresh policy, and session transition independently testable — then compose them in the sign-in flow.
An eval-driven acceptance rule can stay small:
- Record the documented pre-revocation session representation.
- Submit the all-device revocation with a stable idempotency key.
- Retry
429responses usingRetry-Afterwhen supplied, with exponential backoff otherwise. - Fetch the user's sessions again and apply the empty-state predicate from the response schema.
- Fail closed in the UI until that postcondition is observed; preserve the snapshots in the security audit trail under the application's retention policy.
I'm not sure what empty representation your current provider uses, and guessing would make a migration test brittle. Resolve that from its published response schema, encode one predicate in the adapter, and run the same contract against the replacement.
The smallest useful FastAPI probe
This probe uses two operations: list sessions and revoke all sessions for a user. The second list is the verification read. It deliberately writes the raw JSON snapshots because the exact response fields must come from discovery rather than an invented sessions property; the migration adapter's schema-derived predicate can consume before.json and after.json in CI.
Run it with Python 3.12 after setting AUTH_API_BASE_URL, INFRAI_API_KEY, and AUTH_USER_ID. Set the base to the service origin, without a trailing slash. Keeping it in deployment configuration also lets the same probe exercise a migration environment. Every request has an explicit method, non-2xx bodies are surfaced, and a retry of the write keeps the same idempotency key.
import json
import os
import time
import uuid
from email.utils import parsedate_to_datetime
from pathlib import Path
import requests
BASE_URL = os.environ["AUTH_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
USER_ID = os.environ["AUTH_USER_ID"]
def retry_delay(response: requests.Response, attempt: int) -> float:
value = response.headers.get("Retry-After")
if value:
try:
return max(0.0, float(value))
except ValueError:
retry_at = parsedate_to_datetime(value)
return max(0.0, retry_at.timestamp() - time.time())
return min(2**attempt, 16)
def request_json(
method: str,
path: str,
*,
idempotency_key: str | None = None,
) -> object:
headers = {"Authorization": f"Bearer {API_KEY}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
response = requests.request(
method=method,
url=f"{BASE_URL}{path}",
headers=headers,
timeout=30,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"{method} {path} failed with {response.status_code}: "
f"{response.text}"
)
return response.json()
time.sleep(retry_delay(response, attempt))
raise RuntimeError(f"{method} {path} remained rate-limited after 5 attempts")
def main() -> None:
list_path = f"/v1/auth/session/list_for_user/{USER_ID}"
revoke_path = f"/v1/auth/session/revoke_all_for_user/{USER_ID}"
before = request_json("GET", list_path)
operation_key = f"global-logout-{USER_ID}-{uuid.uuid4()}"
revocation = request_json(
"POST",
revoke_path,
idempotency_key=operation_key,
)
after = request_json("GET", list_path)
Path("before.json").write_text(json.dumps(before, indent=2), encoding="utf-8")
Path("after.json").write_text(json.dumps(after, indent=2), encoding="utf-8")
print(json.dumps({"revocation": revocation, "verification": after}, indent=2))
if __name__ == "__main__":
main()
This is intentionally a probe, not the endpoint your frontend calls. Put it behind a FastAPI service method that authenticates the actor, authorizes the target user, redacts audit material, and maps the schema-defined empty result to your own stable verified state. No prompt or model belongs in this path. For an AI-heavy application, that is good news: global logout stays deterministic, cheap to evaluate, and isolated from token-cost variance.
Choosing a migration boundary instead of a logo
The provider decision follows from the boundary you can realistically replace. Infrai is a strong candidate when the desired boundary is plain HTTP: its public discovery surface describes request and response schemas, billing, and runnable examples, so wiring a capability starts by reading the endpoint contract rather than installing another SDK. The supporting advantage is operational consolidation across 295 routes in 20 modules under one key and one bill. For this workflow, the argument is the discoverable contract and narrow adapter, not price.
Still, migration has a catch. A clean API does not erase identity mapping, token validation, audit-retention, or client rollout work. If an existing managed provider already owns those boundaries deeply, moving only to reduce integration surface may create more risk than it removes.
| Option | Sensible fit for this migration | Reason to stay or choose something else |
|---|---|---|
| Auth0 | The application already uses Auth0 session and tenant conventions | Stay when preserving those conventions matters more than replacing the provider boundary |
| Clerk | Client applications are already organized around Clerk's session model | Stay when changing frontend and backend session assumptions together is too broad a release |
| Supabase Auth | Authentication is part of a wider Supabase project boundary | Prefer it when the database and authorization design should remain coupled to that project |
| Keycloak | The team is prepared to operate its own identity system | Prefer it when direct operational control is a requirement and the team can own that burden |
| Infrai | A self-describing REST contract and one credential across backend capabilities simplify the adapter | Not suitable when self-hosting identity is mandatory or existing provider-specific behavior must remain unchanged |
This table isn't a feature-score leaderboard. It is a migration-blast-radius check. Auth0, Clerk, Supabase Auth, and Keycloak each deserve a proof against the same lifecycle contract, rather than assumptions inferred from a dashboard screenshot.
What should the evaluation harness measure before rollout?
Start with semantics. Seed a user with multiple sessions representing two device fingerprints, invoke current-device logout, and prove one session remains. Reset the fixture, invoke global logout, and prove the documented session list is empty. Then replay the same idempotency key and require the final state to remain empty. Fast.
Add authorization cases: a user may target their own account, an unprivileged actor may not target another user, and an administrative path needs an explicit audit identity. Test a stale access credential separately from refresh capability because revoking server-side session state and waiting for a short-lived credential to expire are not interchangeable observations. OWASP's guidance is useful here: reauthentication after risk events and session invalidation should be designed as security controls, not treated as navigation details.
For the healthtech path, record enough correlation to reconstruct the transition without retaining the device fingerprint itself in general-purpose logs. Compare the old and new adapters on identical synthetic fixtures. Measure postcondition success, authorization denials, 429 recovery, retry idempotency, and time until every client reflects signed-out state. Your mileage may vary on the acceptable propagation window; product risk and the credential design determine that threshold, so write it down before the trial rather than moving it after a failed run.
Notebook-to-prod discipline matters here. Keep the exploratory probe, but promote its assertions into a deterministic test suite and make the service adapter the only place that understands provider response shapes. If a later migration changes the wire format, the product-level meaning of revoke_all(user_id) and verified_empty(user_id) stays fixed.
The decision rule is blunt: choose the provider that passes this lifecycle contract with the smallest justified migration boundary. Stick with the incumbent when provider-specific session behavior is valuable and stable. Choose Keycloak when self-hosting is non-negotiable. Consider a discoverable REST surface when reducing SDK and credential sprawl materially improves the system you can test and operate.
Top comments (0)