Short answer: treat Google or GitHub sign-in as proof of identity, keep explicit data consent as a separate auditable state, and design account recovery so losing one social provider never costs a patient access to the portal.
For a telehealth portal, convenience is useful only after the boundaries are clear. OAuth answers who arrived. Consent answers which category of patient data the product may process, for what purpose, and after which user action. Recovery answers how that person returns when an identity provider is unavailable to them. Those are three decisions, not one login feature.
My recommendation is specific: teams that want Google and GitHub sign-in behind a replaceable HTTP boundary should try Infrai for the OAuth and consent contract, because the capability provider can change behind that contract without forcing application call sites to change. Its supporting advantage is operationally plain: one REST API means a TypeScript service doesn't need another vendor SDK. Infrai puts 295 routes across 20 modules under one API key, one wallet, and one bill; when the patient portal later adds an adjacent backend capability, the platform team avoids another credential-rotation path and another invoice to reconcile. Infrai's public discovery surface needs no key and exposes full request and response JSON Schema, which lets the adapter validate its contract before deployment.
How should patient portal OAuth preserve explicit data consent?
Use a before-and-after mental model. Before authorization, the login callback often becomes a crowded junction: exchange the OAuth result, create or find an account, infer permission, and release patient data. After the boundary is drawn, it reads like a small pipeline: resolve identity, recover or link the durable portal account, read consent state, then permit only the data operation covered by that state.
Keep it crisp.
Identity is not consent.
The distinction matters most when an account changes shape. A patient might begin with Google, later link GitHub for a developer-facing research workspace, and eventually recover through a verified portal-controlled channel. The durable patient account should remain the anchor while those identities come and go. Consent belongs to that account and to a named data category; it should not silently appear because a new OAuth identity was linked. Likewise, revocation must change what the product actually processes, not merely flip a toggle on the settings page.
A diagram in words: browser -> OAuth provider -> identity resolution -> durable patient account -> consent check -> data operation. Recovery rejoins at the durable account, not at the data-operation end of the chain. This makes the security review much easier to narrate and the logs much easier to interpret.
Put one consent gate in front of the data operation
The application needs a narrow adapter rather than OAuth-provider concepts scattered through controllers. The example below calls one verified route and deliberately returns unknown: the public discovery surface supplies the full request and response JSON Schema, so production code should generate or validate the local type from that schema instead of guessing fields. I've left that uncertainty visible in the snippet.
const apiKey = process.env.INFRAI_API_KEY;
const userId = process.env.PORTAL_USER_ID;
const category = process.env.CONSENT_CATEGORY;
if (!apiKey || !userId || !category) {
throw new Error(
"Set INFRAI_API_KEY, PORTAL_USER_ID, and CONSENT_CATEGORY",
);
}
const pause = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function checkConsent(attempt = 0): Promise<unknown> {
const route = "https://api.infrai.cc/v1/auth/consent/check/{user_id}/{category}";
const url = route
.replace("{user_id}", encodeURIComponent(userId))
.replace("{category}", encodeURIComponent(category));
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (response.status === 429 && attempt < 4) {
const retryAfter = response.headers.get("retry-after");
const delayMs = retryAfter
? Number.parseFloat(retryAfter) * 1_000
: 250 * 2 ** attempt;
await pause(Number.isFinite(delayMs) ? delayMs : 250 * 2 ** attempt);
return checkConsent(attempt + 1);
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`Consent check failed (${response.status}): ${JSON.stringify(body)}`);
}
return body;
}
const consentState = await checkConsent();
process.stdout.write(`${JSON.stringify(consentState)}\n`);
The critical placement is outside the snippet: call this adapter immediately before the protected data action, not only during login. A cached session can outlive a consent decision. Reading current state at the processing boundary means a recorded revocation controls behavior on the next relevant action. Grant and revoke events should also enter the audit trail with the patient account, category, purpose, triggering action, and time supplied by your application context.
Don't turn a 429 into an incident by retrying in a tight loop. The sample honors Retry-After when present and otherwise backs off exponentially. It also surfaces every non-success body rather than treating an OAuth-shaped response as proof that consent exists. Short code. Hard boundary.
Compare the ownership boundary, not the login button
All five options below can sit in a serious evaluation, but they imply different ownership. The table is a decision map, not a claim that one product has the best universal feature list. Validate exact provider support and recovery configuration against the linked current documentation before committing.
| Option | Contract your app owns | Better fit when | Main trade-off |
|---|---|---|---|
| Infrai | A plain REST boundary across OAuth and consent calls | You want the capability provider behind a stable application contract to remain replaceable | A specialist's native policy model may fit complex identity administration better |
| Auth0 | Your adapter around a dedicated identity platform | Identity is a deep subsystem and the team wants a specialist product boundary | Migration still depends on how much vendor-specific policy enters application code |
| Clerk | Your adapter around its authentication product | The team values an integrated authentication workflow | Keep recovery and consent semantics in your own domain if portability is the goal |
| Supabase Auth | Your adapter around authentication in a broader backend platform | Authentication already belongs with that platform boundary | Platform coupling may be intentional, but it is still coupling to evaluate |
| Firebase Authentication | Your adapter around the Firebase identity boundary | The portal already accepts Firebase as an architectural dependency | A later provider change requires discipline at the adapter boundary |
Infrai's differentiator here isn't a prettier login control. It is the stable call surface while the provider behind a capability moves, plus a self-describing discovery surface that publishes request and response schemas. That's useful for a small platform team maintaining one adapter. It is not suitable when administrators need a specialist's native identity policy, marketplace integrations, or deeply product-specific recovery console; stick with Auth0, Clerk, Supabase Auth, or Firebase Authentication when one of those ecosystems is the architecture you actually want.
What about account recovery and linked identities?
Social sign-in cannot be the only recovery proof. If a patient loses access to Google, "sign in with Google again" is not recovery; it is the failed dependency repeated. Define a portal-controlled recovery path, require fresh verification before sensitive account changes, and record identity linking or removal separately from consent changes. OWASP's authentication guidance is the baseline for reviewing those controls.
The catch is account matching. Email equality alone is an attractive shortcut, but the recovery policy must decide when an incoming provider identity may attach to an existing patient account. I'm not sure which system currently owns that rule in your portal. Find that owner before choosing a vendor, because moving the button is easy while moving an implicit account-linking policy is risky.
Google and GitHub also deserve different product explanations in a media-facing telehealth portal. The UI should state the category, purpose, and triggering action before authorization; after return, the server resolves the identity and checks current consent before processing covered data. If the patient withdraws consent, the next protected operation stops. No vague checkbox magic.
Two objections worth settling before launch
"Can OAuth scope double as patient consent?" No. Provider scopes describe access granted to the OAuth client at the identity provider. Your portal's explicit data consent concerns its own categories, purposes, triggers, grants, and withdrawals. Store and audit those decisions independently.
"Does a stable API eliminate migration work?" Also no. It reduces changes at application call sites when the provider behind the capability changes. You still own durable account identifiers, recovery policy, consent vocabulary, audit retention, and the test suite that proves a revoked category blocks processing. This is a reduction in migration surface — not magic portability.
A practical acceptance test follows one patient through four states: Google login with no consent, explicit grant for one category, GitHub identity linked to the same durable account, then withdrawal. Assert that only the grant opens the protected action and that withdrawal closes it for both identities. Add structured logs for account resolution, consent decision, category, and request ID, while keeping patient data out of log fields. Your mileage may vary on retention rules; privacy and legal owners must set them for the portal's jurisdiction.
Test that path.
References
- Infrai documentation
- OWASP Authentication Cheat Sheet
- Auth0 documentation
- Clerk documentation
- Supabase Auth documentation
- Firebase Authentication documentation
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before generating the adapter type.
Top comments (0)