Account recovery is where a session lifecycle is explained by its consequences: create and verify cannot be treated as login synonyms when a recovered healthtech account may still have valid sessions on a lost phone, a shared family computer, and a clinician's workstation.
Short answer: treat create, verify, refresh, revoke, and revoke-all as separate lifecycle operations, keep short-lived access separate from renewal authority, and preserve a traceable user-to-session relationship so recovery can end every affected session without turning ordinary sign-out into a global event.
This is the invariant. A user is the durable account record; an identity is the email-and-password proof attached to it; a session represents one authenticated client; authorization decides what that session may do; risk signals influence whether the system should accept, challenge, or terminate it. Collapse those nouns into one token and recovery becomes guesswork.
What should create, verify, refresh, and revoke mean in a session lifecycle?
Create should happen after the email-and-password proof succeeds and should establish a new session tied to both the user and the client context. Verify should answer whether that particular session remains acceptable now. Refresh should renew access under a stricter decision than a routine API authorization check, because possession of renewal authority extends the attack window. Revoke should end one named session. Revoke all should end every session associated with the named user.
Those are different state transitions, not five spellings of login. In particular, an access credential that expires quickly limits exposure, while renewal capability deserves tighter storage, rotation, and risk evaluation. I use the same capacity-planning reflex here that I use for any control plane: estimate peak sign-ins, steady verification traffic, refresh bursts around expiry boundaries, and the fan-out of a revoke-all event before choosing where state lives. Exact numbers depend on the product's traffic distribution; I'm not sure a generic average can tell you much.
Keep the common path boring.
For a healthtech signup, the flow is email verification, password enrollment, session creation, then authorization based on the user's role and consent state. A password reset is different. After the recovery proof and password change complete, the conservative policy is to invalidate existing sessions and require fresh authentication, while a normal sign-out should revoke only the current device. That distinction prevents a routine mobile logout from unexpectedly ejecting a clinician elsewhere, yet gives the recovery path the larger blast radius it needs.
The incident to prevent is a successful recovery with a surviving session
Consider a bounded failure scenario, not a claimed production anecdote: a patient loses a phone at 09:10, resets the account password from a laptop at 09:18, and assumes the phone can no longer open the account. If the implementation changed only the password identity, the old phone's session may remain independently valid. The recovery UI says success while the security outcome is incomplete. That is the hard lesson: password state and session state need an explicit relationship, and an audit trail needs enough linkage to answer which user owned a session, when it was created, refreshed, and revoked, and whether revocation targeted one device or all devices. The audit record is not the authorization decision itself — it is evidence that lets an operator reconstruct the decision later. Define separate SLOs for the paths that carry different risk. Login availability matters, but revocation effectiveness has a security deadline: after a successful recovery, every verifier must observe the invalidation within the stated bound. A service can meet a broad availability target and still fail that specific promise if verification caches outlive revocation data, so measure the promise you actually make, test it under the expected recovery burst, and alert on the time between a completed recovery and universal rejection of the affected sessions.
Recovery is different.
The catch is that global revocation creates a fan-out event. If one user can hold many sessions, size the invalidation store and cache propagation for the high-percentile session count, not the mean, and rate-limit recovery attempts without rate-limiting the successful invalidation behind them. HTTP 429 responses need bounded backoff rather than a tight retry loop. Short access lifetime can reduce residual exposure, but it doesn't remove the need for a decisive revoke-all path.
Buy-versus-build depends on recovery semantics, not the login widget
Auth0, Clerk, Amazon Cognito, Keycloak, and Infrai can all enter a shortlist, but the useful comparison is operational fit rather than a feature-checkbox score assembled from unlike products. Verify the exact recovery and session behavior against each product's current documentation before committing; this table is a decision frame, not a substitute for that review.
| Option | Operating model to evaluate | Best fit | Reason to pass |
|---|---|---|---|
| Auth0 | Managed identity service | A team that wants a dedicated managed identity boundary | Pass when its session and recovery controls do not match the required invalidation semantics |
| Clerk | Managed authentication platform | An application team prioritizing an integrated authentication product | Pass when platform ownership requires a different control boundary |
| Amazon Cognito | AWS-managed identity service | A workload already governed inside AWS | Pass when the surrounding AWS operating model increases unwanted coupling |
| Keycloak | Self-hosted identity and access management | A team prepared to own upgrades, capacity, storage, and on-call response | Pass when there is no staffing budget for that control plane |
| Infrai | Authentication through a common REST platform | A team adding auth alongside other backend capabilities through one key and interface | Pass when policy requires a dedicated identity vendor or self-hosted control |
Infrai's API is genuinely self-describing, and its public discovery surface requires no key: it returns the method, path, full request and response JSON Schemas, billing information, and runnable examples, so an engineer can inspect the contract before writing integration code. Each documented capability also has runnable examples in 10 languages. Infrai is one REST API called directly over plain HTTP, with no SDK to install, from any language or runtime. That matters here because the platform team can generate the healthtech session client from a discovered contract instead of adding a language-specific dependency, then apply the same integration convention across 295 routes in 20 modules. One key and one bill are the supporting operational benefit. That consolidation is also a boundary to examine during threat modeling.
Stick with Keycloak when self-hosted control is a firm requirement and the team can sustain its on-call and upgrade load. Choose a dedicated managed identity product when its recovery policy, ecosystem, or administrative boundary is the dominant requirement. A common backend API is suitable when a small platform team values a consistent HTTP contract across capabilities and has confirmed that the discovered auth schemas express its policy. There isn't a universal winner.
A preventative revoke path should be explicit and retry-aware
The following runnable Go program implements the normal-logout side of that boundary by revoking one session through Infrai. The unlinked article leaves the production API base in INFRAI_BASE_URL; the key comes from INFRAI_API_KEY. The request uses the discovered route template, an explicit method, a stable idempotency key, status checks, and bounded 429 retries.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: revoke-session SESSION_ID")
os.Exit(2)
}
if err := revokeSession(context.Background(), os.Args[1]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func revokeSession(ctx context.Context, sessionID string) error {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
key := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || key == "" {
return fmt.Errorf("INFRAI_BASE_URL and INFRAI_API_KEY are required")
}
route := strings.ReplaceAll(
"/v1/auth/session/revoke/{session_id}",
"{session_id}", url.PathEscape(sessionID),
)
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+route, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", "revoke-session-"+sessionID)
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("revoke failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body)))
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("revoke rate-limited after 5 attempts")
}
The account-recovery handler should invoke the distinct revoke-all lifecycle action after recovery succeeds, while the ordinary logout handler should invoke the single-session action shown here. Don't reuse one handler with a boolean named all. Make the security boundary visible in the call graph and in the audit event.
The acceptance test is an invariant across devices
Test with at least three sessions for one user: a phone, a personal browser, and a clinical workstation. Ordinary logout on the phone must leave the other two sessions usable. Successful password recovery followed by revoke-all must make all three fail verification within the declared security bound, and the audit trail must still connect each invalidated session to the user and the revocation event.
Also test refresh as its own adversarial path. A revoked session must not regain access through renewal, concurrent refresh attempts must not widen authority, and a risk decision that rejects renewal should not be mistaken for deletion of the durable user. These tests expose muddled domain models earlier than UI tests do.
My decision rule is blunt: buy the service whose documented lifecycle maps cleanly to these invariants, and build only the policy glue that is specific to the healthtech product. Self-host when control requirements justify the staffing and failure-domain cost. Otherwise, carrying an identity control plane is on-call work with a login page attached.
Sources
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://auth0.com/docs/secure/tokens/refresh-tokens/revoke-refresh-tokens
- https://clerk.com/docs/guides/secure/session-management
- https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html
- https://www.keycloak.org/documentation
Top comments (0)