OAuth2, API keys, and client assertions
Step-by-step SMART-style client authentication—create a site API key, configure JWKS, scopes, audiences, and exchange a client assertion for an access token.
Xenrad integration uses OAuth 2.0 client credentials with a private_key_jwt style client assertion (JWT bearer). There are no shared client secrets issued by the server. Your system proves identity by signing a JWT with a private key whose public key is registered as JWKS.
Prerequisites
- Admin access in the Xenrad web app to the site you are integrating.
- Permission to create and manage API keys for that site (
site.api_keys.*style permissions in the product). - An asymmetric key pair (for example RSA or EC) on the integration side to sign assertions.
Step 1 — Generate a key pair (on the integrator side)
- Create an RSA or EC key pair in a secure store (HSM, vault, or local dev key for testing only).
- Publish the public half as a JWKS document with a
keysarray (standard JWK format). - Note the kid (key id) if you rotate keys; your JWT header should reference the correct key.
You will either host the JWKS at an HTTPS URL or paste the JWKS JSON into Xenrad when creating the key.
Step 2 — Create an API key in the Xenrad UI
Navigate to the site → API keys (labels may read “API keys” or “integration clients”).
Click Create API key and fill the form. After save, the table shows a Client ID (for example xenrad-…). Copy and store it; it is the OAuth iss / client identifier for assertions.
Field reference (every field)
| Field | Required | Meaning |
|---|---|---|
| Name | Yes | Human-readable label (for example Epic FHIR prod). Shown only in admin UI. |
| Status | Yes | active or disabled. Disabled keys cannot obtain tokens. |
| JWKS URI | One of URI or inline | HTTPS URL returning {"keys":[...]}. The server can fetch your public keys from here. |
| Inline JWKS JSON | One of URI or inline | Paste the same JSON you would serve at the URI. Use when the integrator cannot host JWKS. |
| Token TTL (seconds) | Yes | Lifetime for access tokens Xenrad issues after a successful exchange. Allowed range is 60–3600 seconds; the server enforces bounds. |
| Allowed scopes (comma-separated) | Strongly recommended | Scopes this client may request at the token endpoint. Only scopes listed here can appear on the granted token. Use SMART system scopes (see below). |
| Allowed audiences (comma-separated) | Optional | Valid aud values for access tokens Xenrad issues. Used with the audience parameter on the token request. If you omit the token request’s audience, the server may fall back to the first entry here or an internal default—set this explicitly for clarity. |
| Protocols (comma-separated) | Yes for non-FHIR | Examples: fhir, hl7, hl7-mllp. Default is fhir if empty on create. Controls which integration surfaces treat this client as eligible (MLLP also checks for hl7-mllp). |
| Allowed IPs (comma-separated) | For HL7 MLLP | Remote IP rules for MLLP connections. Supports single IPs, optional CIDR ranges, or * for “any” (use with care). Not used for FHIR HTTP or HL7 HTTP (those use bearer tokens). |
| HL7 sender application | Optional MLLP filter | If set, MLLP messages must match MSH-3 sending application. |
| HL7 sender facility | Optional MLLP filter | If set, MLLP messages must match MSH-4 sending facility. |
Either JWKS URI or inline JWKS is required on create. The server rejects a key with neither.
Scopes you can allow
The admin form may show example placeholders such as fhir.read; the token server only grants SMART-style system scopes. Enter real values like system/Patient.read (comma-separated in the form).
The token server advertises support for SMART-style system scopes (read/write per resource):
system/Patient.read,system/Patient.writesystem/Condition.read,system/Condition.writesystem/AllergyIntolerance.read,system/AllergyIntolerance.writesystem/Observation.read,system/Observation.writesystem/Procedure.read,system/Procedure.writesystem/DiagnosticReport.read,system/DiagnosticReport.writesystem/DocumentReference.read,system/DocumentReference.write
Grant only what the interface needs. FHIR checks a read or write scope per resource and method. HL7 HTTP requires write scopes on Patient, Observation, and DocumentReference for the token (see HL7).
Audiences you should set
Access tokens are JWTs with an aud claim. Different listeners validate different audiences:
- FHIR HTTP expects audience
xenrad-fhirwhen verifying tokens. - HL7 HTTP expects
xenrad-hl7-http.
Put the audiences you need into Allowed audiences, and pass the right one as audience when you POST to the token endpoint (recommended). If you rely on the default list, ensure the first allowed audience or the explicit audience parameter matches the surface you call.
Step 3 — Fetch SMART configuration
GET /.well-known/smart-configuration on the OAuth2 base (see Endpoints reference).
You should read at least:
issuertoken_endpointtoken_endpoint_auth_methods_supported(expectprivate_key_jwt)scopes_supported
Step 4 — Build the client assertion JWT
Create a JWT with:
- alg — match a key your JWKS advertises (for example
RS256). - typ —
JWT. - kid — optional but recommended to select the signing key.
Claims (standard pattern for this integration):
| Claim | Value |
|---|---|
iss | Your Client ID from Xenrad (for example xenrad-uuid…) |
sub | Same as iss |
aud | The token endpoint URL exactly as you will POST to (must match server validation) |
jti | Unique id for this assertion (replay protection) |
exp | Short expiry (assertion is one-time use after validation) |
Sign with your private key.
Step 5 — Request an access token
POST to token_endpoint with Content-Type: application/x-www-form-urlencoded:
| Parameter | Value |
|---|---|
grant_type | client_credentials |
scope | Space-separated list of requested scopes (subset of allowed scopes) |
client_assertion_type | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | The JWT from step 4 |
audience | Optional but recommended: target resource audience, e.g. xenrad-fhir or xenrad-hl7-http |
Successful response JSON includes:
access_token— Bearer token for FHIR or HL7 HTTPtoken_type—Bearerexpires_in— secondsscope— granted scopes (may be subset of request)
Step 6 — Call the API
Authorization: Bearer <access_token>
Use the token only for the surface whose audience you selected. Fetch a new token when expired.
Operational tips
- Rotate keys by adding a new JWK to your JWKS with a new
kid, then updating the integration to sign with the new key, then removing the old JWK after traffic moves. - Disable a key in the UI immediately if a private key leaks; that stops token issuance without deleting audit history (if your deployment retains it).
- Replay: each client assertion must use a fresh
jti; the server rejects duplicates.
Related
- FHIR integration — resource URLs and search.
- HL7 integration — MLLP vs HTTP and required scopes.
- Endpoints reference — base URLs and paths.