Skip to main content

Client Assertion (JWT Bearer)

Client Assertion is an alternative to the Client Credentials flow that authenticates the client using a signed JWT instead of a shared client secret. The client signs the JWT with a private key; Medplum verifies it against the public keys published at the client's JWKS URI.

This approach is specified in RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants and is a cornerstone of the SMART on FHIR Backend Services specification.

When to Use Client Assertion

Prefer client assertion over a client secret when:

  • A specification requires it — SMART Backend Services and some payer integrations mandate JWT-based client authentication.
  • You want to avoid transmitting shared secrets and instead rely on asymmetric cryptography.
  • You need per-instance key rotation without updating a central secret.

Prerequisites

  1. A Medplum account. (If not, please register.)
  2. A ClientApplication — create one on the Project Admin page.
  3. An asymmetric key pair (RSA or EC). See Generating a Key Pair below.
  4. A publicly accessible JWKS endpoint that hosts your public key.

Generating a Key Pair

You can generate a key pair with any standard tool. The examples below use openssl.

EC (P-384, recommended):

# Generate private key
openssl ecparam -name secp384r1 -genkey -noout -out private.pem

# Derive public key
openssl ec -in private.pem -pubout -out public.pem

RSA (2048-bit minimum):

# Generate private key
openssl genrsa -out private.pem 2048

# Derive public key
openssl rsa -in private.pem -pubout -out public.pem

Keep the private key secret. Publish only the public key via a JWKS endpoint (see below).

Medplum supports the following signing algorithms: ES256, ES384, ES512, RS256, RS384, RS512.

Publishing Your Public Key (JWKS)

Medplum fetches your public key at runtime from the URL you configure on the ClientApplication. The endpoint must:

Example JWKS response:

{
"keys": [
{
"kty": "EC",
"crv": "P-384",
"use": "sig",
"kid": "my-key-1",
"x": "<base64url-encoded x>",
"y": "<base64url-encoded y>"
}
]
}

Many platforms (AWS, GCP, Auth0, etc.) can host a JWKS endpoint for you. If you are self-hosting, you can use openssl or a library like jose to convert your PEM public key to JWK format.

Configuring the ClientApplication

Set the jwksUri field on your ClientApplication to the URL of your JWKS endpoint. You can do this in the Medplum App UI or via the FHIR API:

{
"resourceType": "ClientApplication",
"name": "My Backend Service",
"jwksUri": "https://example.com/.well-known/jwks.json"
}

Obtaining a Token

JWT Claims

The JWT you sign must include the following claims:

ClaimRequiredValue
issYesYour ClientApplication ID (the UUID)
subYesYour ClientApplication ID (the UUID)
audYesThe Medplum token endpoint URL (e.g. https://api.medplum.com/oauth2/token)
iatYesIssued-at time (Unix epoch seconds)
expYesExpiration time (Unix epoch seconds); keep this short (≤ 5 minutes)
jtiRecommendedA unique identifier for the JWT to prevent replay attacks

Token Request

Once you have a signed JWT, exchange it for an access token with a client_credentials grant:

On success, the response is a standard OAuth2 token response:

{
"token_type": "Bearer",
"access_token": "<YOUR_AUTH_TOKEN>",
"expires_in": 3600
}

The access_token can then be used in subsequent API requests as a Bearer token.

Error Reference

error_descriptionCause
Invalid client assertionThe client_assertion field is empty or not a valid JWT.
Client not foundThe iss/sub in the JWT does not match any ClientApplication ID.
Client must have a JWK Set URLThe matching ClientApplication has no jwksUri configured.
Invalid client assertion audienceThe aud claim does not match the Medplum token endpoint URL.
Invalid client assertion issuerThe iss claim does not match the ClientApplication ID.
Invalid client assertion signatureThe JWT signature could not be verified against any key in the JWKS.
Unsupported client assertion typeThe client_assertion_type value is not urn:ietf:params:oauth:client-assertion-type:jwt-bearer.

Further Reading