Federated Login with Your Identity Provider
Federated login lets your patients start a HealthEx consent flow already signed in with your identity provider. HealthEx redirects the patient to your IdP, verifies the ID token you return, and matches or creates the patient from its claims — so the patient never creates a separate HealthEx credential.
This is a standard OpenID Connect authorization code flow with PKCE. HealthEx is the Relying Party; you are the OpenID Provider. The flow is Service Provider–initiated: it begins when the patient opens a HealthEx consent link, and HealthEx redirects out to you.
What you need to provide
| Issuer URL | An https URL with no query or fragment, e.g. https://idp.partner.com. Must serve a discovery document (below). |
| Client ID | The client identifier you issue to HealthEx. |
| Client credentials | Either a client secret, or register HealthEx's public keys for private_key_jwt (see Client authentication). |
| Redirect URI allowlist | Allow https://api.healthex.io/hosted-sso/callback on your client. |
To use federated login your organization must first be enabled for it by your HealthEx contact — file a HealthEx Partner support ticket to get started.
Once enabled, an organization administrator allowlists your issuer URL from the HealthEx admin settings. An issuer that has not been allowlisted is rejected before any redirect happens.
Requirements for your IdP
HealthEx validates all of the following. You can ask your HealthEx contact to run a preflight check against your issuer, which reports each item below as pass or fail before any patient is involved.
Discovery
Publish {issuer}/.well-known/openid-configuration containing:
issuer— must exactly equal the issuer URL you registeredauthorization_endpointtoken_endpointjwks_uri, serving at least one keyid_token_signing_alg_values_supportedincluding an asymmetric algorithm (RS*,PS*, orES*)code_challenge_methods_supportedincludingS256token_endpoint_auth_methods_supportedincludingprivate_key_jwtorclient_secret_basic
ID tokens must be signed asymmetrically. Symmetric algorithms such as HS256 are rejected.
ID token claims
| Claim | Required | Notes |
|---|---|---|
email | Yes | Patient matching is keyed on it. A token without email fails the login. |
auth_time | Yes | HealthEx sends max_age, which makes this REQUIRED per OIDC Core §3.1.2.1. |
amr | Yes | Must evidence multi-factor authentication — see below. OPTIONAL in OpenID Connect, but required here: nothing else in the token says how the patient authenticated. |
nonce | Yes | Must echo the nonce from the authorization request. |
given_name, family_name, birthdate, phone_number, address | Recommended | Used to populate the patient record. date_of_birth is accepted as an alias for birthdate. |
Authentication assurance
Federated login is held to NIST 800-63B AAL2 on every login. There is no per-organization exemption.
- Freshness —
auth_timemust be within 12 hours, AAL2's reauthentication bound. HealthEx sendsmax_age=43200, so re-authenticate the patient if their session is older. - Multi-factor —
amrmust show two different kinds of factor, or includemfaalongside at least one named factor. Two factors of the same kind (a password and a PIN, say) do not qualify. Factor kinds are taken from RFC 8176:
| Kind | amr values |
|---|---|
| Knowledge | pwd, pin, kba |
| Possession | otp, hwk, swk, sc, pop, sms, tel |
| Biometric | fpt, face, iris, retina, vbm |
SMS and voice one-time codes (sms, tel) are discouraged by AAL2 but are accepted by HealthEx as possession factors, so a password plus an SMS code satisfies the requirement. SP 800-63B classes authentication over the public telephone network as restricted rather than prohibited: it remains permitted at AAL2 provided users are offered an alternative and given notice.
Accepted combinations
amr | Why |
|---|---|
["pwd", "otp"] | Password + authenticator code — knowledge + possession |
["pwd", "sms"] | Password + SMS one-time code |
["pwd", "tel"] | Password + voice one-time code |
["pwd", "hwk"] | Password + hardware key |
["pwd", "fpt"] | Password + fingerprint — knowledge + biometric |
["sc", "face"] | Smart card + face — possession + biometric |
["sms", "fpt"] | Out-of-band code + biometric |
["pwd", "mfa"] | mfa asserts multiple factors; one named factor alongside it is enough |
["pwd", "otp", "vendor_method"] | Unrecognised values are ignored; the rest still qualifies |
Client authentication
At the token endpoint HealthEx uses the method your client is registered for, provided your discovery document advertises it. Registration is what decides, not the discovery document alone: token_endpoint_auth_methods_supported describes what your server accepts from any client, which is not necessarily how you registered ours.
client_secret_basic— used whenever you have issued us a client secret. HTTP Basic, per RFC 6749 §2.3.1.private_key_jwt— used when no client secret is configured. HealthEx sends a signed assertion instead of a secret. Register the public keys fromhttps://api.healthex.io/hosted-sso/jwksagainst the client ID you issued us. The assertion carriesissandsubset to that client ID,audset to your token endpoint, a uniquejti, and a five-minuteexp.
So to move an existing integration onto private_key_jwt, register our public keys and tell us to clear the client secret; advertising the method alone will not switch it.
If your document advertises neither method, the exchange fails rather than guessing. If the field is absent entirely, client_secret_basic is assumed, per OIDC Core §9.
Registering HealthEx's keys for private_key_jwt
With private_key_jwt there is no shared secret. You register the client ID you issued to HealthEx against HealthEx's public keys, and your token endpoint verifies our assertion signature against them.
1. Fetch the key set
GET https://api.healthex.io/hosted-sso/jwks
No authentication. Public, cacheable, and served with Cache-Control: public, max-age=300.
{
"keys": [
{
"kid": "hx-hosted-sso-2026-01",
"kty": "RSA",
"use": "sig",
"alg": "RS384",
"n": "wJ8f...",
"e": "AQAB"
}
]
}
Only public parameters are published. The set may hold more than one key during rotation — treat it as a set and select by kid, never assume a single entry.
2. Register it against our client ID
In your IdP, on the client record for the client_id you issued HealthEx:
| Setting | Value |
|---|---|
| Token endpoint auth method | private_key_jwt |
| Client secret | None — remove any existing secret |
| Key source | jwks_uri = https://api.healthex.io/hosted-sso/jwks |
Prefer jwks_uri over pasting a static key. A URI lets us rotate keys without a coordinated change on your side; a pasted key means every rotation is a breaking change requiring scheduled work from both parties.
Also confirm your discovery document lists private_key_jwt in token_endpoint_auth_methods_supported — HealthEx reads that field and will not attempt the method if it is absent.
3. What HealthEx will send
At your token endpoint, in place of an Authorization header:
| Form parameter | Value |
|---|---|
client_id | The client ID you issued us |
client_assertion_type | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | The signed JWT below |
The assertion's header carries alg and the kid identifying which published key signed it. Its claims:
| Claim | Value |
|---|---|
iss | The client ID you issued us |
sub | The client ID you issued us |
aud | Your token_endpoint, exactly as it appears in your discovery document |
jti | Unique per assertion — reject replays |
iat | Issue time |
exp | iat + 5 minutes |
Signing algorithm
HealthEx selects a key whose alg appears in your token_endpoint_auth_signing_alg_values_supported. If you omit that field, we use our first published key and its alg. If you publish the field but none of our keys match it, the login fails before any request reaches your token endpoint — so if you constrain algorithms, tell us which one you require.
Verifying your setup
A Global Admin can run the IdP issuer probe against your issuer URL before go-live. It fetches your discovery document and reports whether private_key_jwt is advertised, whether your jwks_uri is reachable, and which client authentication method the live flow will use.
Starting the flow
Append iss to any hosted patient-consent link:
https://app.healthex.io/#/patient-consent/<projectId>/enrollment/link?iss=https://idp.partner.com
iss must be an allowlisted issuer for the organization that owns the project, or it is ignored and the patient sees the ordinary HealthEx login.
Combine it with the parameters from Redirecting Patients After Consent to return the patient to you at the end:
...?iss=https://idp.partner.com&redirectUri=https://partner.com/callback&xid=123-456-000.eyjUKFNMP
The authorization request
HealthEx redirects the patient to your authorization_endpoint with:
| Parameter | Value |
|---|---|
response_type | code |
client_id | The client ID you issued |
redirect_uri | https://api.healthex.io/hosted-sso/callback |
scope | openid profile email unless configured otherwise |
state | Single-use, verified on return |
nonce | Must be echoed in the ID token |
code_challenge / code_challenge_method | PKCE, always S256 |
max_age | 43200 |
HealthEx then exchanges the code at your token_endpoint and verifies the ID token against your jwks_uri.
Security parameters: state, nonce, and PKCE
Three single-use values protect the flow. HealthEx generates and checks all three — you never need to store or validate them yourself. Your only obligation is to handle them per the OpenID Connect and OAuth specifications, which most providers do out of the box.
| Parameter | Generated by | Your obligation | Checked by |
|---|---|---|---|
state | HealthEx | Return it unmodified on the redirect back | HealthEx, before the code is used |
nonce | HealthEx | Copy it into the ID token's nonce claim | HealthEx, after signature verification |
code_challenge / code_verifier | HealthEx | Store the challenge with the code; verify the verifier at the token endpoint | You |
state — ties the response to the request
A single-use random value sent on the authorization request. When your IdP redirects the patient back, HealthEx compares the returned state against the one it stored for that browser session and aborts if they differ.
This is what stops an attacker feeding a HealthEx session an authorization code obtained in a different session — a CSRF-style login attack. Return state exactly as received; do not re-encode, truncate or normalise it.
nonce — ties the ID token to the request
A single-use random value sent on the authorization request that your IdP must copy into the ID token's nonce claim. HealthEx verifies the signature first, then rejects the token if the claim does not match what it sent.
This is the one parameter that requires action in your token issuance: a token without the matching nonce is rejected even if perfectly signed. It prevents a previously issued ID token being replayed into a new login.
PKCE — ties the code to the client that requested it
HealthEx generates a random code_verifier, sends its SHA-256 hash as code_challenge with code_challenge_method=S256, and presents the original verifier when exchanging the code.
This is the one your side enforces. Your token endpoint must reject the exchange if the verifier does not hash to the stored challenge. Support for S256 must appear in code_challenge_methods_supported; HealthEx always sends PKCE on this flow and never falls back to plain.
PKCE binds the authorization code to whoever began the flow, so a code intercepted in transit cannot be redeemed by anyone else.
Lifetime
All three are minted per authorization request, scoped to the patient's browser session, and discarded as soon as the callback is processed — whether it succeeded or failed. None is reusable, so a replayed callback fails on the state check.
Issuer pinning
Separately from these, HealthEx records which issuer it redirected to and re-validates it server-side against your organization's allowlist before the code is exchanged. The issuer used to fetch signing keys is therefore never taken from the returned token — a token cannot nominate where it should be verified.
What the patient experiences
- The patient opens the consent link and is redirected to your IdP.
- They authenticate with you.
- Your IdP returns them to the HealthEx callback.
- HealthEx verifies the token, matches or creates the patient, and continues the consent flow.
- If
redirectUriwas supplied, they return to you withstatus,patientId, andxid.
If the patient already has a full HealthEx session on the device, federated login is skipped and that session is used instead.
Existing patients
A patient whose HealthEx account was established with a stronger method — CLEAR identity verification, or Google, Microsoft or Apple sign-in — is not signed in by a federated assertion alone. HealthEx prompts them to sign in with that existing method first, then links your identity to the account. Every later federated login proceeds without the prompt.
This only applies to accounts that already exist with stronger authentication. A patient onboarded through your IdP signs in normally.
Troubleshooting
| Symptom | Cause |
|---|---|
| Patient sees the ordinary HealthEx login instead of your IdP | iss is not allowlisted for the project's organization, or federated login is not enabled for it |
Issuer is not approved for this organization | The iss returned in the callback does not match an allowlisted issuer |
IdP id_token missing 'email' claim | Add email to the ID token; patient matching needs it |
invalid_client at the token endpoint | The client secret is wrong, or HealthEx's JWKS is not registered for private_key_jwt |
| Login rejected for assurance | auth_time older than 12 hours, amr missing, or amr does not evidence two different factor kinds |