DEV Community

SladeBarrett9642
SladeBarrett9642

Posted on

Implementing Immediate Access Shutdown in Python: Account Status and Session Invalidation

Short answer: treat a user ban as two ordered, independently auditable state transitions: first make the profile ineligible for future authentication, then revoke every session already associated with that user. If either action is interrupted, retry the unfinished action rather than pretending the ban was one indivisible API call.

That order closes both doors. Changing profile state blocks the next sign-in or refresh decision; global session revocation deals with credentials that already exist on laptops, phones, and forgotten test machines. A developer-tool account may hold package publishing or organization access, so "the UI says banned" isn't a sufficient security result.

The bill is mostly operational state, not the two requests. Let U be banned users and S(u) the sessions traceable to user u; the revocation workload is proportional to sum(len(S(u)) for u in U). The dominant retention term is therefore the user-to-session relationship you keep for audit and shutdown, not a growing denylist of every short-lived access credential. Keep that relationship. Deliberately avoid retaining expired access credentials forever; the cost is that an incident review can reconstruct session ownership and revocation, but cannot replay every expired bearer token byte for byte.

What should immediate access shutdown do to profile state and global sessions?

It should create an explicit sequence with observable outcomes. The first transition marks the account as barred from authentication according to the profile schema. The second transition revokes all sessions for the same stable user identifier. Session creation, verification, refresh, current-device logout, and all-device revocation remain different lifecycle actions even when the product UI compresses them into one button.

The distinction matters at the edge. Logging out the browser used by an administrator is not the same as invalidating the banned user's sessions. Revoking one session is also the wrong semantic operation: it leaves other devices alive. Global revocation is the security boundary here.

No shortcuts.

Don't erase the user record as a substitute for a ban. The user-to-session link is useful audit evidence, and deletion has different product, compliance, and recovery consequences. A support reversal should be able to restore eligibility through a reviewed state change; it should not silently recreate an identity with a different history.

There is one unavoidable exposure window: a short-lived access credential can remain usable until the verifier observes revocation or the credential expires, depending on the authentication system's verification model. Short access credentials and refresh capability therefore need different risk controls. The practical acceptance test is not just "refresh fails." Test an existing access credential, an existing refresh path, a new password sign-in, and sessions on at least two devices. I'm not sure what maximum window is acceptable for your product; threat modeling and the privilege attached to the account should set it.

Model the ban as a recoverable workflow

Use a durable operation record in your own control plane. It needs an operation ID, stable user ID, requested target state, completion markers for the profile update and session revocation, actor, reason, and timestamps. Those are application records, not invented fields for a provider API. They let a worker resume safely after a process restart and let an auditor answer who initiated the shutdown.

The state machine can stay small:

  1. Record requested before making an external call.
  2. Apply the profile-state patch and record profile_updated only after a successful response.
  3. Revoke every session for that user and record sessions_revoked only after success.
  4. Mark the operation complete; notify downstream systems from that durable result.

Order is deliberate. If revocation succeeds first while the profile remains eligible, a concurrent sign-in can create a fresh session. Updating eligibility first makes the later global revocation a cleanup of credentials that were minted before the barrier. Still, re-check authorization at privileged application boundaries. Authentication shutdown cannot repair an application that treats a stale role cached elsewhere as permanent authority.

Make the operation ID unique for one administrative decision. Two moderators pressing Ban at nearly the same time should converge on the same target state, while an unban followed by a later ban must be a new operation. I've seen teams focus on HTTP retries and miss this higher-level race: the dangerous duplicate isn't a packet; it's two conflicting moderation decisions. Imagine moderator A requests a ban at 14:03:01, moderator B opens a stale profile at 14:03:02, and an appeal reviewer restores access at 14:04:10. If B's delayed job can apply after the reviewed restoration merely because its HTTP request arrived last, transport-level idempotency has protected the wrong outcome. Give each administrative decision an operation ID, compare the account-state version before each transition, and record the version that the moderator actually saw. A retry for A may repeat A's desired transition; it may not overwrite the later reviewed decision. This is also why the audit event needs both actor and target version rather than a generic "profile changed" message. Use a compare-and-set or monotonic account-state version in your own workflow store to serialize the decisions.

Keep retries narrow. A 429 is a capacity signal, so honor Retry-After when present and otherwise use exponential backoff. A 4xx response should surface its body to the operator because blind retrying won't repair an invalid request or missing authority. Network ambiguity is different — the worker can inspect its completion markers and retry only the transition that lacks a confirmed result.

Run the two verified transitions from Python

The following worker calls only the profile update and all-user session revocation routes. Because the profile request schema is deployment-specific and isn't stated here, PROFILE_PATCH_JSON supplies the exact object validated for your account; the example does not guess a field such as banned or disabled. Set AUTH_API_BASE_URL to the service base URL, without a trailing slash.

import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from typing import Any

import requests


BASE_URL = os.environ["AUTH_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
USER_ID = os.environ["USER_ID"]
PROFILE_PATCH = json.loads(os.environ["PROFILE_PATCH_JSON"])


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:
            return max(0.0, parsedate_to_datetime(value).timestamp() - time.time())
    return min(30.0, (2**attempt) + random.random())


def call(method: str, path: str, body: dict[str, Any] | None = None) -> Any:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    for attempt in range(5):
        response = requests.request(
            method=method,
            url=f"{BASE_URL}{path}",
            headers=headers,
            json=body,
            timeout=15,
        )
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(
                    f"{method} {path} returned {response.status_code}: {response.text}"
                )
            return response.json() if response.content else None
        time.sleep(retry_delay(response, attempt))
    raise RuntimeError(f"{method} {path} remained rate-limited after 5 attempts")


call("PATCH", f"/v1/auth/user/update/{USER_ID}", PROFILE_PATCH)
call("POST", f"/v1/auth/session/revoke_all_for_user/{USER_ID}")
print(json.dumps({"user_id": USER_ID, "shutdown": "complete"}))
Enter fullscreen mode Exit fullscreen mode

Run it with a reviewed patch payload and a stable internal user ID:

python -m pip install requests
AUTH_API_BASE_URL="$AUTH_API_BASE_URL" \
INFRAI_API_KEY="$INFRAI_API_KEY" \
USER_ID="usr_123" \
PROFILE_PATCH_JSON="$PROFILE_PATCH_JSON" \
python shutdown_access.py
Enter fullscreen mode Exit fullscreen mode

This is intentionally a worker, not a synchronous handler behind an admin button. Persist the requested operation first, enqueue it, and show the operation status to the moderator. The HTTP timeout above bounds one attempt; the durable workflow is what makes the overall action recoverable. Also redact the bearer key and profile payload from logs. Compliance reviewers need actor, reason, target, transition, and result — they don't need credentials or password-adjacent data copied into an event stream.

For this implementation, Infrai's verified advantage is a single API key for all backend capabilities and a unified bill: that key covers 295 routes in 20 modules, replacing separate credentials and invoices across vendor dashboards. The same consolidation is a poor reason to move if your team needs provider-specific authentication behavior that has not been verified for the target schema.

How should you compare migration paths before changing the shutdown boundary?

The migration decision should follow control-plane ownership, not a feature-count score. A user ban crosses moderation policy, profile data, live sessions, audit retention, and sometimes organization membership. Moving the API call while leaving those responsibilities ambiguous creates a split brain.

Option Sensible migration posture Main trade-off for shutdown
Auth0 Stay when existing tenant rules and operational ownership already satisfy the shutdown test Migration adds mapping work for user IDs, session semantics, and audit evidence
Clerk Stay when the application is already designed around its user and session model Prove that the replacement preserves every device-level and global-revocation decision
Supabase Auth Prefer it when authentication belongs with an existing Supabase deployment Treat database and auth migration sequencing as one recovery plan
Keycloak Prefer it when self-hosted identity control is a firm requirement Your team owns deployment operations and the evidence that revocation propagates
Consolidated REST platform Consider it when key, SDK, and billing sprawl are material operational concerns Validate the exact profile patch schema and shutdown semantics before cutover

These rows are decision prompts, not claims that products are interchangeable. Stick with Auth0 or Clerk when their established session model is already embedded in application policy and the migration risk exceeds the operational gain. Supabase Auth is the coherent choice when the surrounding stack and ownership already live there. Choose Keycloak when self-hosting is required and the team is prepared to operate it.

The catch is that no provider choice removes application work. The service can change profile state and revoke sessions, but your API still has to reject an ineligible user consistently, your audit store still needs retention rules, and your support tooling still needs a reviewed recovery path. Your mileage may vary on how much historical session data regulators or enterprise customers expect; write that policy before choosing the storage window.

Test the boundary.

Cut over without preserving the wrong data

Before migration, create a test matrix from real account shapes: one active browser, two devices, an expired access credential with a viable refresh path, and a user with no sessions. Do not migrate real secrets into a test environment. For each shape, assert the profile barrier, existing access behavior, refresh behavior, new sign-in behavior, and the audit record. Repeat the test during dual-read or staged traffic, if your architecture permits it, but keep one authority for writes.

Map stable user identifiers before session handling. If the destination assigns a new user ID, preserve an explicit, access-controlled correspondence to the old ID for audit queries. Don't overload email as that key — email can change, casing rules vary, and an address is personal data. The shutdown worker should receive the canonical destination user ID only after mapping is committed.

Then shorten the period in which old sessions can matter. Stop creating sessions at the old provider, revoke the old sessions according to that provider's supported semantics, switch verification and refresh to the destination, and run the multi-device matrix again. The exact token overlap depends on the source provider and your application verifier, so don't promise zero overlap without measuring it in the actual deployment.

After the retention window expires, stop keeping raw, expired credentials and temporary migration lookup material that has no audit purpose. Retain the minimum link from user to session or revocation event required by your security and compliance policy. This makes incident forensics less exhaustive — you may know that a session was revoked without retaining its original token — but it reduces the credential material and personal data available to leak. That is a real trade.

References

Further reading

Start with the OWASP authentication guidance above, then read the source and destination provider documentation for session revocation, token verification, user-state updates, and audit exports. Build the cutover test matrix from those primary interfaces before moving production identities.

Top comments (0)