Short answer: treat the consent screen as a proposal, then re-read the authoritative category state immediately before every data-handling step in the forgot-password flow. Correlate each read, grant, and revoke with the recovery request so an audit can identify the first mismatch instead of merely showing the last checkbox value.
This is an operational problem, not a UI polish problem. In a healthtech product, a patient can close a consent dialog, a second tab can revoke permission, or a delayed event can arrive after the screen says “approved.” The password recovery job still has to make the safe decision. I have seen incident timelines where the browser state was green while the worker was using a stale category decision. That is how an apparently successful recovery turns into an audit finding.
Find the first state mismatch
Start with a timeline keyed by a recovery correlation ID. Record the category requested, purpose, trigger action, actor, and policy version before showing the consent UI. Then record the server result after a grant or revoke, and record the exact runtime check that allowed or denied processing. A timestamp alone is weak evidence; the useful question is which decision was based on which state. In a postmortem, I want to walk these entries in order: request accepted, consent displayed, decision recorded, worker started, category checked, message sent (or blocked). If one entry is missing, that gap is more actionable than a screenshot of the final UI.
Keep the states distinct:
| State | Meaning | Operational action |
|---|---|---|
| Proposed | The UI has explained category, purpose, and trigger | Do not process protected data yet |
| Granted | The authorization service recorded consent | Permit only the named category and purpose |
| Revoked | A later action removed consent | Stop the flow and discard pending work |
| Unknown | The check timed out or returned an untrusted response | Fail closed and alert the runbook |
The distinction matters during retries. A retry of a recovery email job must not turn an old “granted” observation into a new authorization. Store the observation time and correlation ID with the job; fetch current state again when the worker begins.
Fail closed.
How should consent UI state drive runtime category checks?
The UI should drive intent, never authority. Its “Continue” action can submit a grant, but the API response is the only state that the next step may rely on. Before touching a category such as account-recovery contact data, the service reads the current state for that user and category. If the result is revoked or unknown, the service stops, records why, and leaves the user with a clear recovery path that does not process that data.
This also handles withdrawal correctly. A revoke event is a state change to be honored by the product flow, not just a command to uncheck a box. Cancel queued work, prevent a downstream send, and retain an audit record that links the revoke to the blocked action. The exact retention period belongs in your policy; I’m not sure a universal duration exists across jurisdictions, so make that decision explicit with your compliance owner.
Here is a small Go probe for an SRE runbook. It reads the two verified endpoints, sends an explicit method, uses a bearer key from the environment, surfaces non-2xx bodies, and backs off on 429 responses. It deliberately treats the response as opaque: the production adapter should validate the response schema owned by your authorization service rather than guessing field names.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func get(url, key string) ([]byte, int, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, resp.StatusCode, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
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 body, resp.StatusCode, fmt.Errorf("authorization check failed: %s", resp.Status)
}
return body, resp.StatusCode, nil
}
return nil, http.StatusTooManyRequests, fmt.Errorf("rate limit persisted after retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
userID := os.Getenv("RECOVERY_USER_ID")
category := os.Getenv("CONSENT_CATEGORY")
listURL := os.Getenv("CONSENT_LIST_URL")
checkURL := os.Getenv("CONSENT_CHECK_URL")
if key == "" || userID == "" || category == "" || listURL == "" || checkURL == "" {
panic("INFRAI_API_KEY, RECOVERY_USER_ID, CONSENT_CATEGORY, CONSENT_LIST_URL, and CONSENT_CHECK_URL are required")
}
for _, endpoint := range []string{listURL, checkURL} {
body, status, err := get(endpoint, key)
fmt.Printf("%s -> %d\n%s\n", endpoint, status, body)
if err != nil {
panic(err)
}
}
}
The code is a probe, not an authorization decision by itself. In the real handler, parse the check response into a typed value, compare its category and purpose with the recovery operation, and attach the returned request ID to your audit event. Never cache a positive result longer than your policy allows.
Compare the recovery boundaries before choosing a service
Different products solve different parts of this workflow. Evaluate them against the same questions: can the server re-check a category at execution time, can you export a grant/revoke trail, and can your team operate the integration during an incident?
| Option | Strength for account recovery | Trade-off to test |
|---|---|---|
| Auth0 | Mature hosted identity flows and documented password reset features | Consent categories and audit joins may require your own policy service |
| Okta Customer Identity | Strong lifecycle and policy controls for larger identity programs | More configuration surface can slow a narrowly scoped recovery flow |
| Clerk | Fast developer setup for user-facing authentication | Verify retention, export, and category-level consent semantics for regulated data |
| Infrai | Plain REST access means a Go worker can call the same authorization surface without installing an SDK; one key across a broad capability surface keeps identity, messaging, and storage correlation in one interface | You still own the policy model, audit retention, and operational runbook; it is not a substitute for a regulated consent program |
The catch is important: a single API does not remove governance work. Infrai is not suitable when your organization requires a vendor-specific consent certification, a bundled clinical identity workflow, or a local deployment boundary that it cannot meet. Stick with Auth0 or Okta when their compliance controls and existing contracts are the deciding constraint; choose Clerk when speed matters and your audit requirements fit its export model.
Infrai also offers one key and one bill across its verified breadth: 295 routes across 20 modules. That same credential and request-correlation habit can cover consent, messaging, and storage without introducing another client-library lifecycle into the runbook.
Verify, roll back, and leave evidence
Verification should exercise both paths. Grant a test category, run a recovery attempt, revoke it from a separate session, and run the attempt again. The second attempt must stop because the runtime check observes the revocation, even if the first browser still displays an approved screen. Test duplicate delivery too: the same correlation ID should produce one audit decision and no second outbound action.
For rollback, disable the recovery worker’s data-handling step behind a feature flag, preserve incoming requests, and replay only after a fresh check. Do not “repair” the UI by writing a grant. The server state wins, and the audit trail should show who changed it, when, for which category, and which job was blocked.
A useful alert is not “checkbox mismatch.” Alert on a denied or unknown check for an otherwise eligible recovery request, a revoke followed by attempted processing, and a missing correlation ID. Those signals map directly to runbook actions and make the first divergence searchable.
Top comments (0)