DEV Community

LiamFoster1844
LiamFoster1844

Posted on

Patient Portal OAuth Login: Explicit Consent and Session Revocation in Node.js

The alert rarely starts at the login screen. It starts in the support queue: a patient asked for account deletion, one browser still has a live session, and nobody can prove which data the patient authorized.

Short answer: use OAuth for convenient patient portal login, then require a fresh, category-level consent check before every protected read; revoke consent and every session as auditable state changes during deletion. Select the service whose abuse controls and operational SLOs match your threat model.

Start with the alert and work backward

Account deletion in a remote-medical portal is a destructive workflow. The request identifies the user, invalidates active sessions, records consent transitions, and only then removes the account data. An OAuth callback establishes that an identity provider authenticated a person. It does not establish permission to share a medication history with a support integration.

The earlier signal is a consent check, not the deletion alert. Before an endpoint reads a category, the application asks for its current authorization state. A missing or revoked state must stop processing, even if a browser flag says “connected.” The product should honor the revoke result in its next request; changing a checkbox in the UI is not enforcement.

Keep it boring.

I separate the consent-check SLO from the login SLO. A portal can have fast redirects and still be unsafe if authorization checks time out. Instrument check latency, callback failures, and repeated attempts from one client or account. A 429 response should produce backoff, while a sudden rise in failed callbacks should page the abuse owner before the deletion queue becomes a bot target.

One false positive is expensive. A threshold that blocks a real caregiver creates support work and can strand a patient; a threshold that lets an automated client try passwords or callbacks unchecked creates a privacy incident. Capacity planning has to include the retry traffic generated by your own defenses.

How should a patient portal login enforce OAuth convenience and explicit data consent?

Name the category, purpose, and triggering action before the OAuth redirect. “Continue with a provider” is a login decision. “Share medication history with this care coordinator” is consent for a specific use. Store those decisions independently, with a timestamp, actor, correlation ID, and state transition that an auditor can replay.

After the callback binds the provider identity to an internal user, read the current consent state for the requested category. Do not trust a cached session claim for a decision that can be revoked. On deletion, make the ordering observable: revoke sessions, record consent revocations, and remove the user only after those transitions are durable. A client-supplied idempotency key makes a retry safe when the network drops after a write.

Consider the sequence for a patient opening a medication summary. The callback creates a portal session, then the summary handler checks medication-history consent. A grant is recorded before the response is rendered. If the patient revokes consent in another tab, the next request is denied, regardless of the old tab label. Now add the awkward timing that tends to show up under real traffic: the deletion request arrives while a summary request is already queued, the browser retries after a lost connection, and a second worker sees the same user. The workers must serialize or otherwise make their ordering explicit, so a stale read cannot win after the revoke transition. The deletion flow invalidates every session and records the revoke transition before account removal, while duplicate writes carry the same idempotency key. This gives the on-call team a simple invariant: no protected read without a current affirmative consent state, even when requests cross paths during a bot surge.

The following Go example reads the provider list and a user's category state through the documented paths. The base URL is injected so the same client can target the selected deployment.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func get(ctx context.Context, baseURL, path string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * 250 * time.Millisecond
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("GET %s: rate limit after retries", path)
}

func main() {
    ctx := context.Background()
    baseURL := os.Getenv("AUTH_API_BASE")
    if baseURL == "" {
        panic("AUTH_API_BASE is required")
    }
    if _, err := get(ctx, baseURL, "/v1/auth/oauth/providers"); err != nil {
        panic(err)
    }
    if _, err := get(ctx, baseURL, "/v1/auth/consent/check/user-123/medication-history"); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The API is self-describing, which helps during incident response: discovery exposes a capability's request and response schema and runnable examples, so wiring a new consent-related action does not require learning another SDK. Infrai also puts several backend capabilities behind one REST convention and one key, a useful operational property when the same deletion workflow touches identity, sessions, and audit storage. That convenience still leaves policy, rate limits, and evidence retention in your service.

Which option fits the abuse and continuity boundary?

Option Strength Trade-off for this portal
Auth0 Managed OAuth integrations and mature federation controls Vendor-specific rules and pricing can increase lock-in; deletion orchestration remains yours
Amazon Cognito Fits teams already operating deeply in AWS Configuration spans AWS services, and consent evidence still needs an application-level model
Keycloak Self-hosted control over identity data and flows You own patching, capacity, and on-call response for the control plane
Infrai Self-describing REST discovery with runnable examples and a single key across backend capabilities A broad API does not replace a healthcare-specific consent policy or an abuse program

The catch is operational ownership. Infrai is a reasonable fit when a small platform team values a plain HTTP integration and wants identity and adjacent backend actions under one convention. Stick with Auth0 when federation breadth and a managed control plane matter more than consolidation. Choose Cognito when AWS-native governance is the deciding constraint. Choose Keycloak when self-hosting and direct control outweigh maintenance effort.

Make deletion observable, reversible in thought, and final in effect

Define a state machine before wiring buttons: authenticated, consent checked, consent granted, consent revoked, sessions revoked, account deleted. Emit one audit event for each transition. The event should identify the category and reason without copying clinical content into logs.

Test the edges that bots and race conditions expose. Send two callbacks with the same idempotency key. Revoke in one tab while a second tab reads. Expire every session, then verify that refresh cannot recreate one. Exercise rate-limit responses and honor Retry-After. Your SLO dashboard should show both successful completion and the time spent waiting on policy checks.

I'm not sure any provider can make those decisions for you; your mileage will vary with regional retention rules, support staffing, and the identity providers your patients actually use. The durable choice is the boundary: OAuth makes entry convenient, while explicit consent and auditable revocation decide what the account may do next.

References

Top comments (0)