<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CianWinslow371</title>
    <description>The latest articles on DEV Community by CianWinslow371 (@cianwinslow371).</description>
    <link>https://dev.to/cianwinslow371</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4091514%2F32878243-4cf1-49fe-8c51-07e166d9697d.png</url>
      <title>DEV Community: CianWinslow371</title>
      <link>https://dev.to/cianwinslow371</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cianwinslow371"/>
    <language>en</language>
    <item>
      <title>Patient Portal Login With Convenient Authorization and Explicit Data Consent</title>
      <dc:creator>CianWinslow371</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:39:42 +0000</pubDate>
      <link>https://dev.to/cianwinslow371/patient-portal-login-with-convenient-authorization-and-explicit-data-consent-3ggf</link>
      <guid>https://dev.to/cianwinslow371/patient-portal-login-with-convenient-authorization-and-explicit-data-consent-3ggf</guid>
      <description>&lt;p&gt;Use delegated login for authentication convenience, but record patient consent as a separate, versioned domain event inside the portal. The deciding constraint is auditability: an identity provider can establish who completed a login, while the portal must establish which data-use statement that person accepted, for which purpose, and under which policy revision. A managed-provider migration should preserve those two claims independently, including the forgot-password path.&lt;/p&gt;

&lt;p&gt;That separation is the architecture decision. It prevents a successful OAuth callback from becoming accidental evidence of consent, and it lets an audit reconstruct the decision without treating transient browser state or an identity-provider log as the ledger of record.&lt;/p&gt;

&lt;p&gt;Keep the boundary sharp.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should patient portal login separate OAuth convenience from explicit data consent?
&lt;/h2&gt;

&lt;p&gt;The portal should model three distinct transitions: identity proof completed, authenticated session established, and consent accepted. They often occur within seconds, but proximity isn't equivalence. The authentication result answers "who controls this account now?"; consent answers "what did this patient affirm, for what stated use, under which text?" A single success flag cannot answer both questions without erasing information that an auditor will later need.&lt;/p&gt;

&lt;p&gt;The core invariants are straightforward, although enforcing them across retries and migrations takes care. A consent record is append-only; it names a stable subject identifier, a purpose identifier, the exact policy version, the decision, and the server-recorded time. Repeated submission of the same decision with the same idempotency key has one durable effect. A later policy revision creates a new record rather than editing the old one. A session may be authenticated while still barred from protected patient data because current consent is absent. Password recovery restores account access; it does not grant, refresh, or imply consent.&lt;/p&gt;

&lt;p&gt;This is an exactly-once business effect built on operations that may execute more than once. Network delivery does not promise a single attempt, so the database transaction must make retries boring: insert the idempotency claim, append the consent event, and update the current projection together. If the transaction loses the race on the idempotency key, read and return the committed result. Don't manufacture a second audit event.&lt;/p&gt;

&lt;p&gt;A useful audit record also avoids collecting incidental material. Store the evidence needed to explain the decision, not authorization codes, access tokens, password-reset secrets, or the full contents of every request. OWASP's authentication guidance treats authentication responses and reauthentication as security-sensitive behavior; the same discipline belongs in recovery and consent flows. Sensitive tokens should remain short-lived protocol material, not become permanent audit payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariants and failure boundaries
&lt;/h2&gt;

&lt;p&gt;The browser is outside the trust boundary for durable consent. It may carry an opaque state value through an authorization redirect, but the server must bind that state to the intended session and validate the returned flow before establishing a local session. Consent then happens on a portal-controlled screen whose policy version is selected by the server. A checkbox sent with an OAuth callback is weak evidence because it mixes two state machines and leaves the portal unable to prove which rendered policy the user actually saw.&lt;/p&gt;

&lt;p&gt;Failures must stop at their own boundary. If authentication is incomplete, no local authenticated session exists. If consent persistence fails, authentication can remain valid, but access requiring that consent stays denied and the user can safely retry. If projection refresh lags after a committed append, authorization should derive from the committed record or fail closed rather than guess. The audit writer belongs in the same transaction as the consent decision; an asynchronous best-effort log can be useful for analytics, but it cannot be the only evidence.&lt;/p&gt;

&lt;p&gt;Recovery deserves special treatment during migration. A forgot-password request should return a uniform public response so account existence isn't disclosed, while the internal trail records a correlation identifier and a coarse outcome without storing the reset secret. After recovery, invalidate or reassess sessions according to the portal's policy and require reauthentication before sensitive account changes. None of those actions modifies prior consent. If identity keys change while moving away from a managed provider, preserve an explicit mapping from the old immutable subject to the new internal patient principal, with its own migration audit entry; email address alone is a poor ledger key because it can change.&lt;/p&gt;

&lt;p&gt;There are two denial modes, and confusing them damages both security and supportability. HTTP 401 belongs to a request that lacks acceptable authentication; HTTP 403 fits an authenticated principal who lacks the required consent or authorization. The user-facing page can remain calm, but structured internal reason codes such as &lt;code&gt;authentication_required&lt;/code&gt;, &lt;code&gt;consent_required&lt;/code&gt;, and &lt;code&gt;policy_reacceptance_required&lt;/code&gt; make reconciliation possible without exposing sensitive detail.&lt;/p&gt;

&lt;p&gt;Short failure, narrow blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the migration options
&lt;/h2&gt;

&lt;p&gt;The product decision is secondary to the ownership decision: which system owns the durable subject key, the consent ledger, recovery policy, and export path? Auth0, Okta Customer Identity, and Keycloak are reasonable names to include in a discovery exercise, but this ADR does not infer capabilities from brand labels. Verify the current deployment, export, retention, signing-key, and recovery contracts in a controlled evaluation, because those facts can change and the evidence supplied for this decision does not resolve them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Operational ownership to verify&lt;/th&gt;
&lt;th&gt;Migration question&lt;/th&gt;
&lt;th&gt;Suitable when&lt;/th&gt;
&lt;th&gt;Limitation that matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;Provider operation with portal-owned consent ledger&lt;/td&gt;
&lt;td&gt;Can stable subject mappings and required audit fields be exported and reconciled?&lt;/td&gt;
&lt;td&gt;The team wants delegated identity operations and accepts an external control plane&lt;/td&gt;
&lt;td&gt;Provider logs must not become the sole consent record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta Customer Identity&lt;/td&gt;
&lt;td&gt;Provider operation with portal-owned consent ledger&lt;/td&gt;
&lt;td&gt;Can recovery policy, subject mapping, and signing-key transitions be tested before cutover?&lt;/td&gt;
&lt;td&gt;Central identity administration is an explicit organizational choice&lt;/td&gt;
&lt;td&gt;Coupling grows if portal authorization is embedded in provider-specific claims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak&lt;/td&gt;
&lt;td&gt;Team-operated identity service with portal-owned consent ledger&lt;/td&gt;
&lt;td&gt;Can the team operate upgrades, backups, key rotation, and recovery drills?&lt;/td&gt;
&lt;td&gt;Infrastructure ownership and deployment control justify the operational load&lt;/td&gt;
&lt;td&gt;The team carries more day-two responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A proof of concept should score all three with the same fixtures rather than with screenshots. Start with a pseudonymous patient whose legacy subject is mapped to one internal principal, authenticate, render policy version 7, and pause before acceptance. Submit the acceptance twice with the same idempotency key, once from each of two concurrent workers, then verify that both callers observe the same committed decision and that the ledger contains one durable consent event. Next, begin another acceptance while the policy registry advances to version 8; the server must reject the stale choice or preserve it explicitly as version 7, never relabel it after the fact. Recover the password, confirm that the old consent event remains unchanged, rotate a signing key, revoke the session, export the subject map, and reconcile every internal principal against both the identity export and consent ledger. Finally, execute the rollback drill and repeat authorization from the prior identity adapter: the patient may need to authenticate again, but the portal must reach the same consent decision from its own record. Record the inputs, correlation identifiers, transaction outcomes, row counts, and keyed digests at each step. The evaluation artifact is this machine-readable trace and reconciliation report, not a slide deck, because a reviewer needs to reproduce why access was allowed or denied rather than trust a demonstration that covered only the happy path.&lt;/p&gt;

&lt;p&gt;I'm not sure which option will satisfy a particular organization's compliance review because retention periods, regional controls, contractual duties, and threat models aren't specified here. Resolve that uncertainty with written control requirements and evidence from the evaluated release, then have security, privacy, and clinical governance sign the decision record. A generic feature matrix can't settle a compliance boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path in Go
&lt;/h2&gt;

&lt;p&gt;The application service below keeps protocol login outside the consent write. Its interfaces are deliberately generic: migration can replace the identity adapter without rewriting the consent invariant, while the transaction owns idempotency, the append-only event, and the current authorization projection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;consent&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrConsentRequired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"current consent required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AcceptCommand&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PrincipalID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Purpose&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;PolicyVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PrincipalID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Purpose&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;PolicyVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;Decision&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RecordedAt&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Tx&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ClaimIdempotency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AppendConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;SetCurrentConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principalID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;ExistingConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WithinTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;
    &lt;span class="n"&gt;Now&lt;/span&gt;   &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;AcceptCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PrincipalID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrincipalID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Purpose&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PolicyVersion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PolicyVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RecordedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithinTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="n"&gt;Tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;claimed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaimIdempotency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExistingConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetCurrentConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrincipalID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PolicyVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database must enforce uniqueness on the idempotency key; checking in application memory is not enough. The production event would also carry a generated event identifier, the policy document's immutable digest, the authentication-session correlation identifier, and actor context appropriate to the organization's privacy rules. Those fields are omitted from the compact example because their concrete types depend on the ledger and policy registry, not because they are optional design questions.&lt;/p&gt;

&lt;p&gt;Deployment follows a dual-read reconciliation period, not a flag day. First establish the internal principal and legacy-subject mapping, then mirror only the minimum authentication outcome needed for comparison while keeping one authoritative consent ledger. Exercise recovery before moving traffic. During cutover, compare counts and keyed digests across old and new identity mappings, investigate every orphan, and retain a signed decision record describing the acceptance threshold. Rollback should switch the authentication adapter without rolling back newly recorded consent events.&lt;/p&gt;

&lt;p&gt;Test the ugly paths. Run the callback twice, submit consent concurrently with the same idempotency key, change policy version between page render and submission, expire the session before acceptance, request recovery for both known and unknown addresses, and replay an old callback state. Assertions should cover database rows and authorization decisions, not only HTTP status codes. Observability should count outcomes by non-sensitive reason code and correlation identifier; keep tokens, health data, and policy text out of routine logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option and its valid use case
&lt;/h2&gt;

&lt;p&gt;The rejected design is to treat successful OAuth login, or acceptance of an identity provider's general terms, as patient consent for portal data use. It is unsuitable when the portal must prove purpose-specific, versioned, revocable consent: the evidence has the wrong owner and usually the wrong semantic scope. It also makes provider migration a consent migration, creating risk that should never have been coupled to login.&lt;/p&gt;

&lt;p&gt;The catch is that a separate consent ledger adds a policy registry, an authorization check, reconciliation work, and an operational duty to protect another sensitive record set. For a low-risk developer tool that needs authentication but has no purpose-specific data-consent requirement, stick with delegated login and ordinary terms acceptance; building a consent state machine there may add ceremony without a defensible control objective. For the patient portal, the additional boundary earns its keep.&lt;/p&gt;

&lt;p&gt;The final decision rule is plain: migrate identity providers only after the portal can demonstrate stable subject mapping, idempotent recovery and consent transitions, append-only evidence, and a rollback that leaves the consent ledger intact. Convenience belongs at login. Consent belongs to the data controller's auditable domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>authentication</category>
      <category>oauth</category>
      <category>healthcare</category>
    </item>
  </channel>
</rss>
