API Authentication
Medicus uses JSON Web Tokens (JWT) with JWKS-based verification for authenticating partner API calls.
https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets
What's a JWT?
A JWT is just a small package of data — like { "user": "alice", "role": "admin", "expires": "5pm" } — that gets cryptographically signed so no one can tamper with it.
What's JWKS?
JWKS (JSON Web Key Set) is simply a publicly accessible URL that hosts the public keys needed to verify those JWTs. It looks like:
https://auth.example.com/.well-known/jwks.json
And returns something like:
{
"keys": [
{ "kid": "key-1", "kty": "RSA", "n": "abc123...", "e": "AQAB" }
]
}The full flow, step by step:
- The auth server creates a JWT, signs it with its private key, and sends it to the partner
- The partner includes the JWT in their request header
- Medicus fetches the public key from the auth server's JWKS URL (using the
kidin the JWT header) - Medicus confirms the JWT signature is valid
- Access granted or denied
Why is this better than simple passwords/secrets?
| Old way (shared secret) | JWKS way |
|---|---|
| Both sides hold the same secret | Only auth server holds the private key |
| If your API is hacked, the secret leaks | Hacker only gets the public key — useless for forging tokens |
| Hard to rotate keys | JWKS URL always serves current keys — rotation is seamless |
| Doesn't scale across many services | Any service can verify by hitting the same URL |
The kid (Key ID) detail
Auth servers sometimes have multiple keys (e.g. during rotation). The JWT header contains a kid field that says "verify me with key-1", so that Medicus knows which key from the JWKS to use.
Storing your JWKS URL
For each application, we need to store the JWKS URL. This is simply a whitelisting strategy so that we can prevent malicious third parties spoofing the requests of an application.
Application Restricted vs User Restricted
API calls can either be:
- Application restricted - The consumer application is a "trusted" application and can make requests using their public/private key only
- User restricted - The consumer application needs to additionally obtain authorisation from a Medicus user in order to make requests
Each third party application can have different permissions and JWT tokens per Medicus environment to allow for new endpoints to be added in staging vs existing production endpoints.
Example JWT for application-restricted:
{
"iss": "medicus-test", // app identifier
"sub": "medicus-test", // app identifier
"iat": 1775379798, // issued at (current timestamp)
"exp": 1775379858 // expiry max 60 seconds after iat
}Example JWT for application-restricted (specifying actor):
{
"iss": "medicus-test", // app identifier
"sub": "medicus-test", // app identifier
"iat": 1775379798, // issued at (current timestamp)
"exp": 1775379858, // expiry max 60 seconds after iat
"act": {
"practitioner": {
"name": "Dr Test Jones", // name of user on third party app
"email": "gppartner@medicus.health", // email of user on third party app
"gmc_number": "G123123" // GMC Number of user on third party app
},
"organisation": {
"name": "UCL Hospital", // name of actor organisation (may be different to tenant organisation)
"ods_code": "R125" // ODS code of actor organisation
}
}
}Example JWT for user-restricted - the key difference is “sub” is now the username of the user, and “azp” must be specified:
{
"iss": "medicus-test", // app identifier
"sub": "gppartner@medicus.health", // Medicus user email address / username
"azp": "medicus-test", // azp = (authorizing party) = app identifier
"iat": 1775573970, // issued at (current timestamp)
"exp": 1775574030 // expiry max 60 seconds after iat
}
API Permissions
During the initial configuration, the consumer application is granted permissions to each API endpoint within their use case.
If a request is made to an endpoint that the application does not have the permission to access then the request will be rejected.
Additionally, each Data Controller (e.g. GP Practice) must enable the consumer application on their Medicus site.
User Restricted Authorisation Workflow
1 ) The consumer application redirects the user to the “authorise third party application” page, passing in the Medicus tenant ID, your application ID and callback URL:
{{base_url}}/{{tenant_id}}/staff/authorise?app={{application_id}}&redirect={{callback_url}}E.g.
https://staging.england.medicus.health/a30005/staff/authorise?app=medicus-test&redirect=https://partner-callback.com/medicus-callback.html2 ) This launches Medicus and if the user is logged in then they are re-directed to a page that asks the to approve the request.
If they are not logged in then they are prompted to login and then re-directed to the correct place.
3 ) Once the user has approved the request, Medicus persists a new staff authorisation for the application and sets the expiryDateTime to now + 12h.
The window will redirect to the callback URL provided in the initial request.
There is no further token exchange required. Medicus records that this user has granted access to the application for 12 hours, and validates this if the application makes a user-restricted request as the Medicus user.
For certain API endpoints (such as Get Care Record) Medicus enforces additional checks that the user specified in the API request is able to access that patient record.
FAQs
-
Q: For testing API authentication, can we provide multiple JWKS URLs per Medicus environment so that each of our engineers can generate their own?
- A: No, but a JWKS file can contain multiple public keys and different kids
-
Q: Do you cache the JWKS file?
- A: Not at the moment but we likely will in the future so please bake this into your implementation (e.g. for key rotation)
-
Q: Does the user restricted callback URL have to be https:// or could it be our own URI scheme?
- A: No, you could use a custom URI. We use this standard Javascript at the end of the user-restricted auth flow: window.location.replace(self.redirectUrl);
-
Q: Are there any specific requirements on kid naming convention?
- A: There is no enforced convention. Your kid can be anything ("key-1", a UUID, a timestamp, etc.). The only requirement is that the kid in the JWT header matches a kid in the JWKS exactly
-
Q: Is the app registration (client_id, JWKS URL) done once globally for the app or per-practice?
- A: There is a single registration per app for ease of onboarding - practices then simply "enable" your app on their Medicus site