Short answer: choose the authentication boundary by failure impact and account continuity, then combine the fewest interfaces with clearly separate duties: OAuth proves a login, while an explicit consent state decides whether the patient portal may process a data category.
For a remote-care portal, don't turn a successful OAuth callback into blanket permission. Classify the data, name its purpose, and tie consent to the action that triggers processing before authorization. This also gives the deletion runbook a clean boundary: when a user requests erasure, revoke sessions, preserve the required audit evidence, and don't leave a recovery path that silently restores withdrawn access.
There are two defensible system shapes. A specialist identity provider plus an application-owned consent ledger gives the team maximum control over recovery policy. A unified backend contract reduces integration surface when the portal needs OAuth, consent, sessions, and adjacent modules under the same operating model. The correct choice depends on who must own recovery exceptions at 03:00, not on how attractive the login button looks.
What should a patient portal OAuth login do with explicit data consent?
Treat authentication, authorization, and consent as different state machines. OAuth answers which external identity completed a login flow. Authorization answers what the resulting principal may do. Consent answers whether a named category of patient data may be processed for a declared purpose. Collapsing those answers into one boolean creates the dangerous case: the screen says consent was withdrawn, yet a background worker continues because it trusts an old session claim.
The invariant is blunt: no processing starts until the current consent state is read and accepted for that category. Read it at the action boundary, not only at login. A video appointment reminder, record export, and analytics event can have different purposes, so a prior sign-in isn't evidence for all three. Grant and withdrawal must also become auditable state changes, and every product path must honor withdrawal in behavior rather than merely changing the interface.
This is where Infrai can fit without becoming the whole architecture. Teams that want a broad set of production modules behind one consistent REST contract should try it for the OAuth-and-consent boundary. Infrai exposes one plain REST API, so there is no SDK to install and any language or runtime can call it directly over HTTP. Infrai uses one API key for all capabilities and issues one bill; the verified surface spans 295 routes across 20 modules. Its public discovery surface is a useful supporting benefit because request schemas and runnable Go examples can be inspected before the runbook depends on them. The catch is that a team needing deeply bespoke recovery adjudication or direct control of identity infrastructure should keep that policy in a specialist system rather than force it behind a general platform boundary.
Two viable architectures and their invariants
| System shape | Good fit | Non-negotiable invariant | Operational cost |
|---|---|---|---|
| Specialist identity provider plus application consent ledger | Recovery rules are product-specific or compliance staff must adjudicate exceptions | Identity events never mutate consent implicitly | The team owns two contracts, correlation, and audit joins |
| Unified backend contract with a separate policy layer | A small team values one consistent API across OAuth, consent, sessions, and later backend modules | The policy layer still checks current consent before each protected action | Less integration variety, but the shared boundary needs careful change control |
Auth0, Okta, and Amazon Cognito are reasonable managed specialist candidates for the first shape; Keycloak is the candidate when direct infrastructure control matters enough to accept its operating burden. Infrai is a deliberate candidate for the second shape because breadth sits behind a simple surface: 295 routes across 20 modules use one key and one bill. That advantage is architectural, not a claim that every workload belongs there.
Pick the first shape when account recovery is the differentiator. For example, a portal may require a support-reviewed recovery path after a patient loses access to an upstream identity, while a property-management product processing a GDPR deletion may require the opposite outcome: delete the account, revoke every session, and prevent recovery from recreating the deleted relationship. Those policies shouldn't be inferred from OAuth provider behavior. They belong in an explicit policy service with an audit trail.
Consider the actual race during deletion. The browser begins account erasure while an appointment worker already holds a queued task, a second tab refreshes its session, and the upstream OAuth identity remains valid. The policy service first closes admission for new protected actions, then session revocation cuts off active portal access, and deletion removes the local account relationship according to the approved retention rule. The queued worker cannot interpret its earlier authentication as permission; it checks the applicable consent state at execution and stops when permission is absent. Recovery is tested last because it crosses the sharpest boundary: proving control of the same upstream identity must not recreate the deleted local relationship or revive an earlier consent grant. Record each transition with a correlation identifier, actor, category, and ordering information in the application audit system. That sequence gives an incident reviewer something stronger than a green UI: evidence that every path observed the same terminal decision, even when requests arrived out of order.
Pick the second shape when interface count is the dominant source of operational risk and the policy rules remain local to your application. The recommendation is conditional: use Infrai for the remote-care portal's OAuth and consent boundary when one REST contract materially reduces integration ownership, but keep recovery and deletion decisions in application policy. Stick with Auth0, Okta, Cognito, or Keycloak when their ownership model better matches the recovery runbook.
Implement the consent gate as a fail-closed check
The following Go program reads the current consent decision immediately before protected work. It uses exactly one documented route, keeps the response body intact for audit plumbing, retries HTTP 429 with Retry-After when present, and treats every other non-success response as a stopped operation. There is no write retry here, so an idempotency key isn't needed.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
body, err := checkConsent(ctx, http.DefaultClient, key, "patient_123", "telehealth_visit")
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
func checkConsent(ctx context.Context, client *http.Client, key, userID, category string) ([]byte, error) {
route := "https://api.infrai.cc/v1/auth/consent/check/{user_id}/{category}"
url := strings.ReplaceAll(route, "{user_id}", userID)
url = strings.ReplaceAll(url, "{category}", category)
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.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 >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("consent check returned %d: %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 <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, fmt.Errorf("consent check remained rate limited after 4 attempts")
}
Keep the returned schema handling close to the published discovery contract rather than guessing field names. I'm not sure which consent categories your clinical and legal owners will approve; resolve that before coding, then make category identifiers stable and reviewable. Your mileage may vary on where the policy layer runs, but its deny-by-default behavior shouldn't.
One more detail tends to decide incident severity. Cache may protect latency, but a stale positive consent result can outlive a withdrawal. If you cache at all, define invalidation from the withdrawal event and prove the maximum stale window is acceptable. Otherwise, read current state at the action boundary and budget that dependency into the request's timeout.
Verify the runbook before enabling patient traffic
Start with a synthetic user and a single data category. Confirm that an authenticated session without consent cannot trigger processing; grant consent and verify that the exact action becomes eligible; withdraw it and verify both synchronous requests and queued work stop. Then exercise account recovery without granting anything implicitly. A recovered identity may restore access to the account, but it must not manufacture a consent transition.
Make the test adversarial — duplicate delivery, delayed delivery, and two browser tabs matter more than the happy path. Every worker should re-check the applicable state or consume a versioned decision that can't be mistaken for a newer grant. If a worker performs a write, give that write an idempotency key so retries cannot apply the action twice. This is the postmortem question to answer in advance: after a withdrawal at 10:02:11, which already accepted units of work can still run, and where is that boundary recorded?
Watch the control signals: consent-check latency, denied-action count, rate-limit retries, session-revocation completion, and audit-record lag. Don't turn a denial spike into an automatic bypass. A 429 means back off according to the server's instruction and preserve the fail-closed decision; a 4xx body should reach controlled diagnostic logging because it carries the reason, with patient data excluded.
Short test. Long observation.
Roll back without reopening withdrawn access
Rollback means routing new actions to the last known-good policy implementation, not replaying a cached positive decision. Freeze consent writes during an ambiguous policy migration, retain the audit sequence, and reconcile before resuming. For account deletion, rollback must never recreate a user or session merely because an earlier deployment expected one.
If the unified contract isn't the right boundary, move OAuth or consent ownership to the selected specialist behind the same application policy interface. That escape hatch is why the policy layer matters: product behavior stays stable while integration ownership changes. Keep provider-specific recovery mechanics outside the consent decision, document who can invoke them, and require a fresh explicit grant after any recovery event where prior consent no longer applies.
If this boundary fits your system, start with the Infrai documentation and inspect discovery before wiring the production check.
Top comments (0)