Application-restricted RESTful APIs - signed JWT authentication
Learn how to integrate your software with our application-restricted RESTful APIs - using our signed JWT authentication pattern.
Overview
This page explains how to integrate your software with our application-restricted RESTful APIs.
In particular, it describes the 'signed JWT' authentication pattern. This pattern is a form of OAuth 2.0 authorisation known formally as private key JWT.
For a full list of available patterns, see Security and authorisation.
When to use this pattern
Use this pattern when:
- accessing an application-restricted RESTful API
- the API uses signed JWT authentication
This pattern is more secure than API key authentication - we use it for APIs that involve personal or sensitive data.
How this pattern works
In this pattern, you authenticate your application by sending a signed JSON Web Token (JWT) to our OAuth 2.0 authorisation server. You provide us with your public key and sign the JWT with your private key. In return, we give you an access token, which you then include with each API request.
The following diagram illustrates the pattern:
The following sequence diagram shows how the various components interact:
In words:
-
The end user launches the calling application.
-
Time passes, until the user needs to access an application-restricted API.
-
The calling application generates and signs a JWT, using its own private key.
-
The calling application calls our OAuth 2.0 token endpoint with the signed JWT. In particular, this uses the OAuth 2.0 client credentials flow.
-
We check the signature against the application's public key, and return an access token to the calling application.
-
The calling application calls the application-restricted API, including the access token.
Tutorials
Detailed integration instructions
The following sections explain in detail how to use this security pattern.
Environments and testing
As well as production, we have a number of test environments. In the steps below, make sure you use the appropriate URL base path:
Environment | URL base path | Availability |
---|---|---|
Development | dev.api.service.nhs.uk/oauth2 | Limited to specific APIs - check the 'Environments and testing' section of your API specification |
Integration test | int.api.service.nhs.uk/oauth2 | All APIs |
Production | api.service.nhs.uk/oauth2 | All APIs |
For most APIs, our sandbox environment is open-access, so you don’t need to complete these steps for sandbox testing.
For more information on testing, see Testing APIs.
Step 1: Register your application on the API platform
To use this pattern, you need to register an application. This gives you access to your App ID and API Key, which you will need later in the process.
- If you do not already have one, create a developer account.
- Navigate to my developer account and sign in.
- Select 'Environment access'.
- Select 'Add new application'.
- Enter the details of your application including application owner and application name to create your new application.
- Select 'View your new application' to check or edit your application details.
- Click the 'Edit' button to make a note of the API key. If you are editing the security details for production applications, follow the online instructions to set up mobile authentication.
- Click the 'Add APIs' button to add the API you want to use.
Step 2: generate a key pair
You need to generate a private/public key pair for each application you created in Step 1 to access testing or production environments. It must be a 4096-bit RSA key pair.
Note that if you generate your own JWKS file, you must use the RS512 algorithm to do this.
Decide on your Key Identifier (KID) - a unique name to identify the key pair in use. The KID will be used to refer to the key pair when constructing and posting the JWT.
We recommend:
- test-1 for testing
- prod-1 for production use
If you create multiple applications to test across multiple test environments, you need a different KID and key pair for each environment.
If you create subsequent key pairs for key rotation, number them sequentially, for example test-2, test-3 and so on. You can use the KID to do zero down time key rotation, simply by publishing the old and new in your JWKS until any JWKS caching time periods have elapsed.
Do not re-use a KID.
For development and integration test environments only, you might find it easiest to use an external key generator to create a private-public key pair, and a JWKS file. Do not use this for a production environment.
For production environments (or test environments), generating your own public-private key locally is much more trustworthy.
Generate a private/public key pair using an external key generator - for test environments only
There are several external key generators available on the internet, and while we cannot endorse any one in particular, we know people have had success with https://mkjwk.org/.
To use it, enter:
Key Size: | 4096 |
Key Use: | Signature |
Algorithm: | RS512 |
Key ID: | <your KID, for example test-1> |
Show X.509: | Yes |
This produces:
- "Public Key" - a JWK that you can use to create a JWKS file for uploading (see below)
- "Private Key (X.509 PEM Format)" - your private key in PEM format
- "Public Key (X.509 PEM Format)" - your public key in PEM format
Important - always keep your private key private. Do not send it to us!
To generate a JWKS file from the public key JWK, you need to wrap it with a keys array, for example:
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "YOUR_KID",
"alg": "RS512",
"n": "gY1TGIkH4B872V2xBYFScYvyGjcd_VVVDQiHcTBG-OKbZlFOgrTIAvJNVtBhW4-meJWxIYtPchnykhLOvSPEOImUcypvP5aRw8KxCjPpKlBEMrRmMYf34zT7sP-IBTPsw3TxLLX0YPR59lX8S64dt7uV3Sgn1fXBlZENCDJhLOP6pwpOdyeezrPJDsKmm5CPh1yQ6774cLNbz9HEg5Sxdno1oAYq4ANmJWNntMi6K4z8Mq5TtAe5sakBzr4UV3sLE77ex76gykGDEidi-vlWbig0ITqLvQ4J8TYOHSq7mzrnRbFgfnersGLjNp25aajSnwt29dog77njZiEtj7zb6fQZx47arBUByWqVkuvsN6CbM94xoWdPGD1Pdi6Kd1CoFPDhWWaJgpYhdZl6amXL_Am4Y1nYAowWgY4M5o9BzuCXkG5obZw1n9HZ25qS7Hdjoy510ASWDyF96FlvRpP336iW3utH3pqmED-patuGHvcDWIi1oirlYKwrTRgGbyEuko_MjdJXSSRnzEansD6k4Bp6NNsjpxqTMy1cd9mCRZsd9MNQa4KxY6A2iDAUTLCwwZ9A1DGh2mP7p7AbaP7X_0nxhf4usyc0JriDew5zGnEwdtonOmsPysfSYpV9E7RzLSybTkMoV6oJz9a0mRHT5hsMgb12HBHqvU3jlzNbnt8"
}
]
}
Go to Step 3.
Generate your own private/public key pair - for production or test environments
On Windows, the easiest way to get the BASH shell tools to do this is to install Git For Windows.
On Linux and Mac OS, the BASH shell comes as standard.
Open a BASH shell command prompt and define your KID:
KID=YOUR_KID
Then run both of the following commands:
- openssl genrsa -out $KID.pem 4096
- openssl rsa -in $KID.pem -pubout -outform PEM -out $KID.pem.pub
These commands create the following files:
- YOUR_KID.pem - your private key in PEM format
- YOUR_KID.key.pub - your public key in PEM format
Important - always keep your private key private. Do not send it to us!
If this is a key pair for a production application, and you want us to host your public key, go to Step 3.
If this is a key pair for development or integration testing environments, or a production environment key you want to host yourself, you also need to create a JWKS file to upload.
To do this, first get the "modulus" of your private key, by entering the following BASH shell commands:
MODULUS=$( openssl rsa -pubin -in $KID.pem.pub -noout -modulus `# Print modulus of public key` \ | cut -d '=' -f2 `# Extract modulus value from output` \ | xxd -r -p `# Convert from string to bytes` \ | openssl base64 -A `# Base64 encode without wrapping lines` \ | sed 's|+|-|g; s|/|_|g; s|=||g' `# URL encode as JWK standard requires` )
Next, build your JWKS file (using the RS512 algorithm) from your KID and public key modulus by entering the following BASH shell commands:
echo '{
"keys": [
{
"kty": "RSA",
"n": "'"$MODULUS"'",
"e": "AQAB",
"alg": "RS512",
"kid": "'"$KID"'",
"use": "sig"
}
]
}' > $KID.json
This creates your JWKS file YOUR_KID.json for uploading in Step 3.
Step 3: register your public key with us
There are two ways to do this - either host your own internet facing public key, or ask us to host it for you.
We recommend hosting your own as it makes you more self-sufficient - you will not need to contact us later to do zero down time key rotation.
Host your own public key
To do this, for applications in development or integration test environments:
- Create an internet facing JWKS endpoint to publicly host your public key and note the URL.
- Sign in to your developer account.
- Select 'Environment access'.
- Select the application you want to add your JWKS endpoint to.
- Edit the public key URL. If you are editing the security details for production applications, follow the online instructions to set up mobile authentication.
- Enter the URL of your JWKS endpoint and click Save.
You do not need to apply any security to your JWKS endpoint, as there is no sensitive information. You can publish it as a static file, for instance on a Content Delivery Network (CDN).
If this public key is for a production application, contact us and tell us:
- your application ID
- the public key URL you want to add, or update
Ask us to host your public key
For applications in development or integration test environments:
- Sign in to your developer account.
- Select 'My applications and teams', 'My applications' and then 'Manage your applications'.
- Select the application you want to add your JWKS public key to.
- Edit the public key URL. If you are editing the security details for production applications, follow the online instructions to set up mobile authentication.
- Choose the JWKS file in JSON format for your public key and click Upload.
- Once it's confirmed as a valid public key, click Save.
We use this public key to create a JWKS endpoint to host your public key and link it to your application in the development or integration environment.
For production applications, contact us and make sure you tell us:
- your application’s App ID, from step 1
- your KID, from step 2
- your public key, from step 2, as an attachment in PEM format
- the APIs you want to use
We use this information to create a JWKS endpoint to host your public key and link it to your application in production.
In the future, we hope to make this process more self-service for production applications. You can track progress or vote for this feature on our interactive product backlog.
Step 4: generate and sign a JWT
Before you can call an application-restricted API, you first need to generate and sign a JWT. This happens at runtime, so you need to code it into your application.
A JWT is a token that consists of three parts: a header, a payload and a signature. The header specifies the authentication method and token type. The payload contains data (detailed below) and the signature is used to verify the token itself.
We strongly recommend that you use a library to generate your JWT tokens, as this can be a complicated process to perform by hand.
You can generate a JWT for test purposes at https://jwt.io/.
Header
The JWT header includes the following fields:
Field | Description | Type |
---|---|---|
alg | The algorithm used to sign the JWT, which must be RS512. | string |
typ | The token type - JWT. | string |
kid |
The Key Identifier (KID) used to select the public key to use to verify the signature of the JWT, for example test-1. If you have multiple public/private key pairs, this will be used to select the appropriate public key. |
string |
Example
{
"alg": "RS512",
"typ": "JWT",
"kid": "test-1"
}
Payload
The JWT payload includes the following fields:
Field | Description | Type |
---|---|---|
iss | The issuer of the JWT. Set this to your API Key. | string |
sub | The subject of the JWT. Also set this to your API Key. | string |
aud | The audience of the JWT. Set this to the URI of the token endpoint you are calling, for example https://api.service.nhs.uk/oauth2/token for our production environment. | string |
jti | A unique identifier for the JWT, used to prevent replay attacks. We recommend a randomly-generated GUID. | string |
exp | Expiry time of the JWT, expressed as a Numeric Time value - the number of seconds since epoch (for example, a UNIX timestamp). Must not be more than 5 minutes after the time of creation of the JWT. | number |
Example
{
"iss": "<test-app-api-key>",
"sub": "<test-app-api-key>",
"aud": "https://api.service.nhs.uk/oauth2/token",
"jti": "<unique-per-request-id>",
"exp": <current-time-plus-5mins-from-jwt-creation>
}
Signature
The JWT signature consists of the contents of the header and payload, signed with your private key. We recommend you use a library to generate this.
Assembling the JWT
The complete JWT consists of:
- the header, base64 encoded
- a period separator
- the payload, base64 encoded
- a period separator
- the signature, base64 encoded
Examples
The following code snippets show how to generate and sign a JWT in Python and C#.
If following the python example do not password encrypt the private key when generating and signing a PyJWT.
For the Python example, PyJWT requires the installation of the crypto extra in order to use RSA keys. To install this:
python -m pip install PyJWT[crypto]
import uuid
from time import time
import jwt # https://github.com/jpadilla/pyjwt
with open("jwtRS512.key", "r") as f:
private_key = f.read()
claims = {
"sub": "<API_KEY>",
"iss": "<API_KEY>",
"jti": str(uuid.uuid4()),
"aud": "https://api.service.nhs.uk/oauth2/token",
"exp": int(time()) + 300, # 5mins in the future
}
additional_headers = {"kid": "test-1"}
j = jwt.encode(
claims, private_key, algorithm="RS512", headers=additional_headers
)
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using IdentityModel;
using Microsoft.IdentityModel.Tokens;
namespace csharp.auth;
public class JwtHandler
{
private readonly string _audience;
private readonly string _clientId;
private readonly SigningCredentials _signingCredentials;
public JwtHandler(String keyOrPfx, string audience, string clientId, string kid)
{
_audience = audience;
_clientId = clientId;
if (keyOrPfx.EndsWith(".pfx"))
{
_signingCredentials = FromPfx(keyOrPfx, kid);
}
else if (keyOrPfx.EndsWith(".key"))
{
_signingCredentials = FromPrivateKey(keyOrPfx, kid);
}
else
{
throw new Exception("Can not recognise the certificate/key extension");
}
}
public string GenerateJwt(int expInMinutes = 1)
{
var now = DateTime.UtcNow;
var token = new JwtSecurityToken(
_clientId,
_audience,
new List<Claim>
{
new("jti", Guid.NewGuid().ToString()),
new(JwtClaimTypes.Subject, _clientId),
},
now,
now.AddMinutes(expInMinutes),
_signingCredentials
);
var tokenHandler = new JwtSecurityTokenHandler();
return tokenHandler.WriteToken(token);
}
private SigningCredentials FromPfx(string pfxCertPath, string kid)
{
var cert = new X509Certificate2(pfxCertPath);
return new SigningCredentials(
new X509SecurityKey(cert, kid),
SecurityAlgorithms.RsaSha512
);
}
private SigningCredentials FromPrivateKey(string privateKeyPath, string kid)
{
var privateKey = File.ReadAllText(privateKeyPath);
privateKey = privateKey.Replace("-----BEGIN RSA PRIVATE KEY-----", "");
privateKey = privateKey.Replace("-----END RSA PRIVATE KEY-----", "");
var keyBytes = Convert.FromBase64String(privateKey);
var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(keyBytes, out _);
var rsaSecurityKey = new RsaSecurityKey(rsa)
{
KeyId = kid
};
return new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha512)
{
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};
}
}
Step 5: get an access token
Once you have a signed JWT, you need to exchange it for an access token by calling our token endpoint. This is an HTTP POST to the following endpoint:
https://api.service.nhs.uk/oauth2/token
Note: the above URL is for our production environment. For other environments, see Environments and testing.
You need to include the following data in the request body in x-www-form-urlencoded format:
- grant_type = client_credentials
- client_assertion_type = urn:ietf:params:oauth:client-assertion-type:jwt-bearer
- client_assertion = <your signed JWT from step 4>
Here's a complete example, as a CURL command:
curl -X POST -H "content-type:application/x-www-form-urlencoded" --data \
"grant_type=client_credentials\
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\
&client_assertion=<YOUR-SIGNED-JWT>" \
https://api.service.nhs.uk/oauth2/token
Note: the URL in the above example is for our production environment. For other environments, see Environments and testing.
You will receive a response with a JSON response body, containing the following fields:
- access_token = the access token you use when calling our APIs
- expires_in = the time after which the access token will expire, in seconds
- token_type = Bearer
Here's an example:
{'access_token': 'Sr5PGv19wTEHJdDr2wx2f7IGd0cw',
'expires_in': '599',
'token_type': 'Bearer'}
Error scenarios
If there are any issues with your call to our token endpoint, we return an error response, as follows:
Error scenario | HTTP status | Error code | Error message |
---|---|---|---|
Grant type is missing | 400 (Bad Request) | invalid_request | grant_type is missing |
Grant type is invalid | 400 (Bad Request) | invalid_request | grant_type is invalid |
Client assertion type is missing | 400 (Bad Request) | invalid_request | Missing or invalid client_assertion_type - must be 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' |
Client assertion type is invalid | 400 (Bad Request) | invalid_request | Missing or invalid client_assertion_type - must be 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' |
Client assertion (signed JWT) is missing | 400 (Bad Request) | invalid_request | Missing client_assertion |
Client assertion (signed JWT) is malformed | 400 (Bad Request) | invalid_request | Malformed JWT in client_assertion |
kid header is missing | 400 (Bad Request) | invalid_request | Missing 'kid' header in client_assertion JWT |
kid header is invalid | 401 (Unauthorized) | invalid_request | Invalid 'kid' header in client_assertion JWT - no matching public key |
typ header is missing or invalid | 400 (Bad Request) | invalid_request | Invalid 'typ' header in client_assertion JWT - must be 'JWT' |
alg header is missing | 400 (Bad Request) | invalid_request | Missing 'alg' header in client_assertion JWT |
alg header is invalid | 400 (Bad Request) | invalid_request | Invalid 'alg' header in client_assertion JWT - unsupported JWT algorithm - must be 'RS512' |
sub and iss claims match but are not a valid API Key | 401 (Unauthorized) | invalid_request | Invalid 'iss'/'sub' claims in client_assertion JWT |
sub and iss claims don't match or are missing | 400 (Bad Request) | invalid_request |
Missing or non-matching 'iss'/'sub' claims in client_assertion JWT |
jti claim is missing | 400 (Bad Request) | invalid_request | Missing 'jti' claim in client_assertion JWT |
jti claim has been reused | 400 (Bad Request) | invalid_request | Non-unique 'jti' claim in client_assertion JWT |
jti claim is invalid type | 400 (Bad Request) | invalid_request |
Invalid 'jti' claim in client_assertion JWT - must be a unique string value such as a GUID |
aud claim is missing or invalid | 401 (Unauthorized) | invalid_request | Missing or invalid 'aud' claim in client_assertion JWT |
exp claim is missing | 400 (Bad Request) | invalid_request | Missing 'exp' claim in client_assertion JWT |
exp claim is in the past | 400 (Bad Request) | invalid_request | Invalid 'exp' claim in client_assertion JWT - JWT has expired |
exp claim is more than 5 minutes in the future | 400 (Bad Request) | invalid_request | Invalid 'exp' claim in client_assertion JWT - more than 5 minutes in future |
exp claim is invalid type | 400 (Bad Request) | invalid_request |
Invalid 'exp' claim in client_assertion JWT - must be an integer |
JWT signature is invalid | 401 (Unauthorised) | public_key error | JWT signature verification failed |
Public key not set up | 403 (Forbidden) | public_key error |
You need to register a public key to use this authentication method - please contact support to configure |
Public key misconfigured | 403 (Forbidden) | public_key error | The JWKS endpoint for your client_assertion can not be reached |
Step 6: store token for later use
Your access token lasts for 10 minutes and you can use it multiple times. If you'll be making more than one API call, store your access token securely for later use.
This reduces the load on our authorisation server and also reduces the chance of your application hitting its rate limit.
For details on what to do if your access token has expired, see refresh token below.
Step 7: call the API
Once you have your API key, you can call the application-restricted API.
You need to include the following header in your call:
- Authorization = Bearer <your access token from step 5>
Here's an example, using a CURL command:
curl -X GET https://sandbox.api.service.nhs.uk/hello-world/hello/application \
-H "Authorization: Bearer [your access token from step 5]"
Note: the above endpoint doesn't currently support signed JWT authentication - this is an example only.
Note: the URL in the above example is for our sandbox environment. For other environments, see Environments and testing.
All being well, you’ll receive an appropriate response from the API, for example:
HTTP Status: 200
{
"message": "Hello application!"
}
Error scenarios
If there is an issue with your access token, we will return an error response as follows:
Error scenario | HTTP status |
---|---|
Access token is missing | 401 (Unauthorized) |
Access token is invalid | 401 (Unauthorized) |
Access token has expired | 401 (Unauthorized) |
For details of API-specific error conditions, see the relevant API specification in our API and integration catalogue.
Step 8: refresh token
Your access token expires after 10 minutes. After that, calls to application-restricted APIs will return an HTTP status code of 401 (Unauthorized). We do not include an OAuth 2.0 refresh token with your access token. Therefore, to get a new access token, repeat the above process from step 4.
Last edited: 9 June 2025 3:20 pm