Skip to main content
Creating a new NHS England: Health Education England, NHS Digital and NHS England have merged. More about the merger.

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.

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:

include
access token
include...
Calling Application
Calling Applicati...
Application-Restricted
API 
Application-Restr...
Authorization Server
(OAuth2.0)
Authorization Ser...
include signed JWT,
get access token
include signed JWT,...

Private Keystore
Private Ke...

Public Keystore
Public Key...
May or may not be present
May or may not be...
End user
End user
Text is not SVG - cannot display JWT authentication pattern context diagram


The following sequence diagram shows how the various components interact:

Calling Application
Calling Applic...
generate and sign JWT
generate and sign JWT
Authorization Server
Authorization...
Application-Restricted API
Application-Rest...
do something
do something
launch application
launch application
display home page
display home page
return access token
return access token
get access token
(/oauth2/token)
get access token...
call application-restricted API
(include access token)
call applicat...
End user
End user
time passes
time passes
Viewer does not support full SVG 1.1 JWT authentication sequence diagram


In words:

  1. The end user launches the calling application.

  2. Time passes, until the user needs to access an application-restricted API.

  3. The calling application generates and signs a JWT, using its own private key.

  4. 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.

  5. We check the signature against the application's public key, and return an access token to the calling application.

  6. The calling application calls the application-restricted API, including the access token.


Tutorials

You can learn how to use this security pattern with our tutorials written in:

Bear in mind that the tutorial only teaches you the basic flow. You'll also need to read the detailed integration instructions below to understand how to handle error scenarios.


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.

  1. If you do not already have one, create a developer account.
  2. Navigate to my developer account and sign in.
  3. Select 'Environment access' on my developer account.
  4. Select 'New app'.
  5. Enter details for your application and click 'add application' to register it.
  6. Select your application.
  7. 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. 
  8. 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-2test-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
Show X.509: Yes

This produces:

  • "Public Key" - your JWKS file for uploading
  • "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!

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:

  1. openssl genrsa -out $KID.pem 4096
  2. 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:

  1. Create an internet facing JWKS endpoint to publicly host your public key and note the URL.
  2. Sign in to your developer account.
  3. Select 'My applications and teams', 'My applications' and then 'Manage your applications'.
  4. Select the application you want to add your JWKS endpoint to.
  5. Edit the public key URL. If you are editing the security details for production applications, follow the online instructions to set up mobile authentication.
  6. 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:

  1. Sign in to your developer account.
  2. Select 'My applications and teams', 'My applications' and then 'Manage your applications'.
  3. Select the application you want to add your JWKS public key to.
  4. Edit the public key URL. If you are editing the security details for production applications, follow the online instructions to set up mobile authentication.
  5. Choose the JWKS file in JSON format for your public key and click Upload.
  6. 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.

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 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 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: 29 November 2023 3:55 pm