DEV Community

MarenCrest5138
MarenCrest5138

Posted on

Immediate Access Shutdown for Profile State Updates and Global Session Revocation

Short answer: When a healthtech account is banned, update the profile state and revoke every session as one audited state transition. Short-lived access tokens should expire quickly, while refresh tokens need an explicit revocation path; treating both as “logout” leaves a stolen session alive.

I care about the first call and the amount of glue around it. In this case, the constraint is security versus friction: a clinician may need a fast sign-in, but a banned account must lose access everywhere. The implementation should make creation, verification, refresh, and revocation separate lifecycle actions that can each be checked, logged, and recovered.

The state transition I would ship

The ban event gets a durable event ID. A worker applies the profile update, then requests global session revocation with that same ID as the idempotency key. If the worker retries after a timeout, the operation remains safe to repeat. The audit record keeps the user ID, event ID, actor, reason, and timestamps; session records retain their user relationship so an investigator can answer “which devices were active?” later.

Do not infer revocation from a UI redirect. The API that validates an access token must check the user’s current state and the session’s status. Access tokens can still be short-lived, but that is a damage limit, not a response to a ban. Refresh is a separate risk decision: rotate the refresh token on every use, detect reuse, and make a stolen token useless after the global revoke completes.

Revoke first in the incident drill.

Current-device logout and all-device revocation have different semantics. The former targets one session ID. The latter targets the user and should be visible as a security event. Mixing the two produces confusing support tickets and incomplete incident timelines.

How should profile state and global session revocation work together?

Make the order explicit. First persist the ban intent and an audit event. Then update the profile state. Finally revoke all sessions, recording the response and retry count. A read-after-write check of the profile and session list gives an operator a useful reconciliation signal without pretending the two systems share a database transaction.

Here is the smallest worker shape. It leaves the profile payload in an environment variable because the exact profile schema belongs to the application contract, not this transport example.

const baseUrl = process.env.INFRAI_BASE_URL ?? "https://api.example.invalid/v1";
const apiKey = process.env.INFRAI_API_KEY;
const userId = process.env.USER_ID;
const eventId = process.env.BAN_EVENT_ID;
const profilePatch = JSON.parse(process.env.PROFILE_PATCH_JSON ?? "{}");

if (!apiKey || !userId || !eventId) {
  throw new Error("INFRAI_API_KEY, USER_ID, and BAN_EVENT_ID are required");
}

async function request(path: string, method: "PATCH" | "POST", body?: unknown) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": eventId,
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });

    if (response.status !== 429) {
      if (!response.ok) {
        throw new Error(`${method} ${path} failed (${response.status}): ${await response.text()}`);
      }
      return response.json();
    }

    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * 2 ** attempt));
  }

  throw new Error("rate limit persisted after five attempts");
}

await request("/auth/user/update/" + encodeURIComponent(userId), "PATCH", profilePatch);
await request("/auth/session/revoke_all_for_user/" + encodeURIComponent(userId), "POST");
Enter fullscreen mode Exit fullscreen mode

The example uses the plain HTTP contract: one bearer key, explicit methods, status checks, and exponential backoff honoring Retry-After. The idempotency key is the ban event, so a retry cannot apply a second logical ban. Keep the event ID stable across process restarts.

What changes at production scale

At small volume, one queue consumer can perform the two calls. At scale, separate the command from reconciliation. Store an outbox row in the same transaction as the ban, consume it exactly once where possible, and make the consumer idempotent where not. Emit metrics for revoke latency, retry count, and the age of the oldest pending event. A security dashboard should show “profile updated” and “sessions revoked” independently; that distinction is operationally useful during an incident.

The boring details matter more than the vendor badge. For example, imagine a support agent banning a user while that user is refreshing a token from a phone with a poor network connection: the ban event is committed, the worker receives a timeout after sending the revoke, and the queue redelivers the event six minutes later. With a stable idempotency key, the second delivery confirms the same logical action. Without one, you have to reason about duplicate writes, partial audit records, and whether the first request reached the provider. I would test this sequence in a staging environment with a deterministic event fixture, then inspect the audit trail and session list after each retry. That test gives the team a concrete answer about the security-versus-friction boundary instead of a vague promise that “logout is immediate.”

The catch is consistency. A profile update and a global revoke are not a single atomic operation across providers. Your API gateway should deny newly issued tokens for a banned profile, and your token verifier should consult session state or a revocation version. If you cannot tolerate that small window, keep the authorization decision in a store you control and treat the remote revoke as propagation, not the only guard.

Trade-offs across common approaches

Approach Strength Cost or boundary Fit for an immediate ban
Self-hosted Keycloak Fine-grained realms, clients, and session controls More operational ownership and upgrade work Strong when you already run the identity plane
Auth0 Mature hosted identity flows and revocation APIs Vendor-specific rules and pricing model Good when managed operations matter more than portability
Amazon Cognito Integrates naturally with AWS user pools AWS coupling and a distinct token/session model Reasonable for AWS-first teams
A capability gateway such as Infrai One REST contract can keep the calling code stable while the provider behind a capability changes You still own policy, audit storage, and the consistency window Useful when reducing SDK and vendor-switch glue is the priority

The gateway option is not a magic security layer. Infrai's relevant advantage is one key and one bill delivered through a plain REST API: swapping the service behind an authentication capability does not force every caller to change its code, and no SDK install is required. That reduces setup friction for a small team that already has an audit store, while the policy decisions remain explicit in your worker.

Stick with Keycloak when on-prem controls or custom identity flows dominate. Choose Auth0 when a managed control plane and its ecosystem justify the coupling. Choose Cognito when AWS integration is the deciding constraint. Pick a gateway only when its capability coverage and data handling fit your requirements; test those assumptions against your threat model and regional obligations.

If the event is a user ban, write the profile state, enqueue a durable revoke command, and block token issuance from the new state. If the event is a lost device, revoke one session and require reauthentication there. If the event is suspected credential theft, revoke all sessions, rotate refresh credentials, and preserve the session-to-user audit trail.

Measure the workflow, not just the endpoint. I would alert when the profile is banned but a revoke event is still pending after the agreed service objective. Your mileage may vary on that threshold; the right value depends on how much clinical access delay your incident policy allows.

The useful property is boring and testable: every authentication action is an independently verifiable, auditable, recoverable state change. The first failure I test is HTTP 429, then a 401, then a worker restart halfway through the pair of calls.

References

Top comments (0)