Skip to main content

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

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.

This pattern is more secure than API key authentication - we use it for APIs that involve personal or sensitive data.

The following diagram illustrates the pattern:

Graphic showing the pattern

The following sequence diagram shows how the various components interact:

Diagram showing interactions

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.

Tutorial

We don't currently have a tutorial for this security pattern. If you would like to see one, please suggest it via our interactive product backlog.

Detailed integration instructions

The following sections explain in detail how to use this security pattern.

Environments and testing

In the steps below, make sure you use the appropriate URL base path:

Environment URL base path
Sandbox (Hello World API only) sandbox.api.service.nhs.uk/oauth2
Integration test int.api.service.nhs.uk/oauth2
Production api.service.nhs.uk/oauth2

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: create an application

To use this pattern, you need to create an application. This gives you access to your App ID and API Key, which you will need later in the process.

  1. If you don't already have one, create a developer account.
  2. Navigate to my developer account and sign in.
  3. Select 'My applications'.
  4. Select '+ NEW APP'.
  5. Enter details for your application.
  6. In the 'APIs' section, find the testing API you want to use and activate it by clicking the slider.
  7. Select 'CREATE' to create the application.
  8. Make a note of the App ID and API Key.

Step 2: generate a key pair

You need to generate a private/public key pair. It must be a 512-byte RSA version 2 key pair.

For example, to do this on UNIX (Linux, MacOS), run the following command:

ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS512.key
openssl rsa -in jwtRS512.key -pubout -outform PEM -out jwtRS512.key.pub

The resulting files, jwtRS512.key and jwtRS512.key.pub, are the private and public keys respectively. The files are PEM-encoded.

Decide on a 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 subsequent key pairs for key rotation, number them sequentially, for example test-2, test-3 and so on. Do not re-use a KID.

Step 3: register your public key with us

To register your public key, contact us. Make sure you include:

  • the environment you want to access (sandbox, integration test or production)

  • your application’s App ID, from step 1

  • your KID, from step 2

  • your public key, from step 2, as an attachment

We will use this information to create a JWKS endpoint to host your public key and link it to your application. If you want to host your JWKS yourself, send us the URL instead of your KID and public key.

In the future we hope to make this process self-service. 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. In this case, 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 if 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

Example

The following code snippet shows how to generate and sign a JWT in Python:

COPY
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

)

Python

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 query parameters:

  • 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 user-restricted 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 Missing grant_type
Grant type is invalid 400 (Bad Request) invalid_request Unsupported grant_type '<value>'
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 JWT
kid header is invalid 401 (Unauthorized) invalid_request Invalid 'kid' header in JWT - no matching public key
typ header is missing or invalid 400 (Bad Request) invalid_request Invalid 'typ' header in JWT - must be 'JWT'
alg header is missing 400 (Bad Request) invalid_request Missing 'alg' header in JWT
alg header is invalid 400 (Bad Request) invalid_request Invalid 'alg' header in JWT - unsupported JWT algorithm - must be 'RS512'
sub and iss claims match but are not a valid API Key 401 (Unauthorized) invalid_request Invalid sub/uss claims
sub and iss claims don't match or are missing 400 (Bad Request) invalid_request Missing or non-matching iss/sub claims
jti claim is missing 400 (Bad Request) invalid_request Missing jti claim in JWT
jti claim has been reused 400 (Bad Request) invalid_request Non-unique jti claim in JWT
aud claim is missing or invalid 401 (Unauthorized) invalid_request Missing or invalid aud claim in JWT
exp claim is missing 400 (Bad Request) invalid_request Missing exp claim in JWT
exp claim is in the past 400 (Bad Request) invalid_request Invalid exp claim in JWT - JWT has expired
exp claim is more than 5 minutes in the future 400 (Bad Request)  invalid_request Invalid exp claim in JWT - more than 5 minutes in future
Public key not set up or misconfigured 403 (Forbidden) public_key_error

You need to register a public key to use this authentication method - please contact support to configure

 

Step 6: call an application-restricted API

Once you have an access token, you can call an 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 7: 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: 8 February 2021 5:15 pm