DEV Community

FluxH91
FluxH91

Posted on

Patient Account Erasure: Coordinating Profile State Update with Session Revocation

Short answer: treat a healthtech account shutdown as two explicit, auditable state transitions: first make the profile ineligible for authentication, then revoke every session tied to that user, while retaining only the relationship data your security and privacy policies require.

The bill for this workflow isn't mainly the two API calls. It is the cost of retaining searchable user-to-session relationships, checking state during authentication, writing audit evidence, and operating the retry path when a request is interrupted. Before selecting a provider, estimate that dominant term as active sessions x relationship-retention time, then add the request volume generated by verification, refresh, and revocation. If nobody can supply those inputs, a vendor price table gives false precision.

For a GDPR deletion request, the security goal is immediate loss of access; the privacy goal is deletion according to policy; and the product goal is avoiding needless sign-ins for unaffected patients. Those goals pull in different directions. The useful design boundary is one user, not one browser: a current-device logout is a local action, while an account shutdown must invalidate the renewal path across every device.

No shortcuts.

What does access shutdown actually cost us to retain?

Session security depends on a traceable relationship between the user record and every session that can still act for it. Without that index, “revoke all” becomes a scan, a hopeful expiry wait, or an incomplete list assembled from application logs. None is a credible deletion control. The relationship should support four distinct lifecycle actions—create, verify, refresh, and revoke—because collapsing them into a single “logged in” flag hides exactly which authority remains live.

Retention is the hard part.

Retention has two sides. Keep too little and an investigator cannot establish which sessions belonged to the account at shutdown time. Keep too much and the deletion system preserves identifiers or usable credential material beyond its purpose. The change that moves the dominant storage term is to stop retaining active session artifacts after revocation, while preserving only the minimum audit linkage and transition evidence required by the organization's policy. The evidence might establish that a transition occurred without preserving a reusable token. Exact retention periods are jurisdictional and organizational decisions; I'm not sure a generic number would survive review, so counsel, security, and data governance must set it together.

That choice has a cost when something goes wrong. Once usable session material is deliberately discarded, operators can't reconstruct a bearer credential to replay its behavior. That is desirable for security, but it means incident analysis must rely on request IDs, transition timestamps, and the user-to-session lineage retained for audit rather than on the credential itself. A team that needs forensic replay should question that requirement before quietly extending credential retention.

The other major cost is friction. Short-lived access credentials reduce the window in which a stale verifier can accept old authority, while refresh capability deserves stricter controls because it can mint new access. Yet forcing every healthy user through authentication on every request is not a sensible substitute for revocation. Keep access and renewal risks separate: verification answers whether this session is valid now; refresh decides whether authority may continue; global revocation ends the family.

How should a profile state update coordinate global session revocation?

Model the operation as a small state machine rather than a controller method with two incidental calls. The profile transition blocks new authentication decisions. The global session transition removes existing authority. Record the intent before execution, use stable operation identifiers for retries, and treat completion as the point at which both independently observable transitions have succeeded. A network interruption between them is not evidence that either transition should be guessed or skipped.

Ordering matters. Updating profile state first prevents a concurrent refresh or new session from reopening access while revocation is enumerating existing sessions. The reverse order creates a race: a session can disappear and then be replaced before the account becomes ineligible. Even with the safer order, every authentication path still has to respect profile eligibility; session revocation can't compensate for a verifier that never checks current authority.

The following client calls only the two verified routes needed for this operation. It takes the profile patch from PROFILE_PATCH_JSON because field names are contract data, and inventing a status field would make the example dangerous. The operation ID remains stable across retries, Retry-After is honored on rate limiting, and every request declares its method explicitly.

import json
import os
import time
import uuid
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen


API_ORIGIN = os.environ["API_ORIGIN"].rstrip("/")


def retry_delay(value: str | None, attempt: int) -> float:
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            try:
                return max(0.0, (parsedate_to_datetime(value).timestamp() - time.time()))
            except (TypeError, ValueError):
                pass
    return min(2 ** attempt, 30)


def send(method: str, path: str, body: dict | None, operation_id: str) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    payload = None if body is None else json.dumps(body).encode("utf-8")
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Accept": "application/json",
        "Idempotency-Key": operation_id,
    }
    if payload is not None:
        headers["Content-Type"] = "application/json"

    for attempt in range(5):
        request = Request(
            f"{API_ORIGIN}{path}", data=payload, headers=headers, method=method
        )
        try:
            with urlopen(request, timeout=15) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f"HTTP status {response.status}")
                raw = response.read()
                return json.loads(raw) if raw else {}
        except HTTPError as error:
            reason = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == 4:
                raise RuntimeError(f"HTTP {error.code}: {reason}") from error
            time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))

    raise RuntimeError("retry budget exhausted")


def shut_down_account(user_id: str, profile_patch: dict) -> None:
    encoded_user_id = quote(user_id, safe="")
    operation_id = str(uuid.uuid4())
    send(
        "PATCH",
        f"/v1/auth/user/update/{encoded_user_id}",
        profile_patch,
        f"{operation_id}:profile",
    )
    send(
        "POST",
        f"/v1/auth/session/revoke_all_for_user/{encoded_user_id}",
        None,
        f"{operation_id}:sessions",
    )


if __name__ == "__main__":
    shut_down_account(
        os.environ["USER_ID"],
        json.loads(os.environ["PROFILE_PATCH_JSON"]),
    )
Enter fullscreen mode Exit fullscreen mode

Set API_ORIGIN to the documented API origin, provide the bearer key through INFRAI_API_KEY, and pass a contract-validated profile object through PROFILE_PATCH_JSON; no request fields are guessed here. In production, the durable workflow record should also distinguish requested, profile-blocked, sessions-revoked, and completed states. This makes an interrupted execution recoverable without pretending that two remote transitions are one transaction. It also gives an operator a precise restart point: retry the pending transition with the same durable operation identity, observe the resulting state, and advance the workflow only after that state is confirmed. Reissuing both steps blindly would be easier to code, but it would erase the distinction between recovery and a brand-new shutdown request.

There are several failure modes worth naming. A refresh racing with shutdown tests whether profile state gates renewal. A second deletion request tests idempotency. A device presenting an old access credential tests verifier freshness. An audit lookup after personal data deletion tests whether the retained linkage is useful without retaining too much. These are security properties, not happy-path demo steps.

Which provider fits the security-versus-friction boundary?

Provider selection comes after the state model. Auth0, Clerk, Supabase Auth, Amazon Cognito, and Infrai are all real candidates, but their place in a decision depends on the system already deployed and on verified session semantics. Don't infer global revocation from a button labeled “log out”; confirm that the provider can represent current-device logout separately from all-device revocation, then test how profile eligibility affects create, verify, and refresh actions.

Candidate Sensible reason to keep or evaluate it Boundary to verify before committing
Auth0 Keep it when it already owns the application's identity and session lifecycle. Verify the exact all-device revocation and refresh behavior your tenant configuration produces.
Clerk Evaluate it when the product team already uses its authentication workflow. Verify how account state is consulted by existing sessions across devices.
Supabase Auth Keep it when it is already part of the application's data and authentication boundary. Verify that deletion, audit retention, and session invalidation policies align.
Amazon Cognito Evaluate it when the application's identity operations already sit in that environment. Verify propagation behavior and the evidence available to the deletion workflow.
Infrai Evaluate it when a team wants broad backend capability behind one consistent REST contract. Verify the application-specific profile patch and retention policy before deployment.

Infrai puts 295 routes across 20 modules behind one REST API and one key, which makes it strongest when integration sprawl is itself an operational risk. That one credential covers all capabilities, leaving the team with one bill instead of dozens of keys and invoices. Its discovery surface exposes request schema, response schema, billing information, and runnable examples for each capability. For this shutdown workflow, plain HTTP means a Python worker doesn't require a provider-specific SDK. Those facts do not remove the need to validate authorization policy, retention, or the profile patch.

The catch is organizational ownership. A consolidated API is not suitable when policy requires direct vendor contracts, provider-specific controls, or an existing identity platform to remain the system of record. Stick with Auth0, Clerk, Supabase Auth, or Amazon Cognito when one already owns the user-to-session lineage and replacing that boundary would add migration risk without improving shutdown semantics. The recommendation is conditional: choose the option that can prove both transitions, not the one with the shortest quickstart.

For acceptance, I would require evidence for five cases: a patient with no sessions, one active session, several devices, a refresh concurrent with shutdown, and a repeated shutdown request. The expected invariant is compact: after completion, the profile cannot authorize a new session and no session associated with that user can continue or renew. Product friction remains limited to the affected account because ordinary logout retains its narrower, current-device meaning.

References

Further reading

Top comments (0)