Client Assertions and the JWKS URI

Client Assertions and the JWKS URI

On this page

Intro

Mutual TLS (mTLS) is well known as a mechanism for strongly authenticating API requests via client certificates. It can be tricky to manage though, since it requires additional infrastructure, such as a second OAuth token endpoint that requires client certificates, and the behavior of middleware such as proxy servers may also need to be considered.

OAuth enables an alternative strong security option to be used. This involves using JSON Web Tokens (JWT) to authenticate the client, via message level security. This can also be combined with a JWKS URI mechanism for validating received client assertions. This article will provide an overview of the key behaviors.

Client Assertions

The most commonly understood kind of JWT is the access token sent to APIs, but JWTs can also be used for other purposes. A client assertion is a JWT that is directly produced by a client application, using a cryptographic key, and presented as proof of the client's identity.

A number of OAuth related specifications describe the details of client assertions, including RFC7521, RFC7523 and OpenID Connect Core. Each Identity and Access Management (IAM) system will need to interpret the standards, and the JWT Assertions tutorial explains the Curity Identity Server implementation.

JWT Authentication-related Specifications

Use Cases

Client assertions are a strong method of authenticating clients, before returning access tokens that allow access to data. They are part of your security toolbox and could potentially be used in many different application scenarios.

Designs need to account for how cryptographic keys will be distributed and used, as well as which JWT signature algorithm will be used. JWTs are almost always signed using an asymmetric algorithm, so that clients can send proof of ownership without disclosing their private key.

B2B APIs

A company providing APIs to business partners could require the use of client assertions to get access tokens, before the API can be called. In this case the partner sends proof of ownership of a private key they have been issued with, which is then trusted by the company providing the API:

Assertions Overview

Financial-grade Security

In some industry sectors, companies must follow advanced security standards from the Financial-grade API (FAPI) Profile. The advanced profile requires confidential clients to authenticate using either Mutual TLS or client assertions. The latter option is referred to as the private_key_jwt client authentication method.

Dynamic API Onboarding

In some dynamic business scenarios, APIs do not know who their clients will be, but need to provide secure and zero-delay onboarding. This can be managed via Dynamic Client Registration. A JWT client assertion can be used as a DRC client authentication method, to get an initial access token with a dcr scope.

The initial access token is then used to register, after which each client gets its own distinct Client ID. Each API client can then be managed independently, such as revoking access without impacting other clients. The DCR Authentication Methods article provides further details.

Strong Mobile Security

Client assertions can also be used in high security mobile scenarios, where each device uses a distinct private key to sign assertions. This provides the capability for API access from each device to be managed separately. The Mobile Best Practices describes this approach further, including the use of DCR for mobile use cases.

Assertions Mobile

Creating an Assertion

A client assertion is produced by creating a JSON payload and then signing it with a private key. The sub, iss, aud, jti and exp claims are required by the OpenID Connect standard. The security work is done using a JWT security library, which uses a private key in the JSON Web Key (JWK) format:

const assertion = await new SignJWT({
    sub: 'partner-api-client',  
    iss: 'partner-api-client',
    aud: 'https://idsvr.example.com/oauth/v2/oauth-token',
    jti: Guid.create().toString(),
    scope: 'read',
})
    .setProtectedHeader( { alg: 'PS256', kid: myId } )
    .setIssuedAt(Date.now() - 30000)
    .setExpirationTime(Date.now() + 30000)
    .sign(privateJwk);

When creating a client assertion, the sub and iss claims reference the client ID of the calling application, and the audience claim is the token endpoint of the Authorization Server. An example JWT assertion's header and payload are shown below.

{
  "kid": "1",
  "alg": "PS256"
}
{
  "sub": "partner-api-client",
  "iss": "partner-api-client",
  "aud": "https://idsvr.example.com/oauth/v2/oauth-token",
  "jti": "7eb87efb-2094-ecf0-531a-357c1d570582",
  "scope": "read",
  "iat": 1651070751703,
  "exp": 1651070811703
}

This JWT is then sent in a POST request to the token endpoint of the Identity Server, which cryptographically verifies it, then returns an access token to the client.

POST https://idsvr.example.com/oauth/v2/oauth-token
content-type: 'application/x-www-form-urlencoded'
accept: 'application/json'

grant_type=client_credentials&
client_assertion=eyJraWQiOiIxIiwiYWxn...
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

Key Distribution

Private keys to sign assertions can be managed in various ways. One option is for them to be distributed to clients by either the API owner or a trusted third party. In other cases the client can issue their own private keys and be responsible for publishing the corresponding public keys.

Keys can be used in various formats, such as password protected PKCS#8 or PKCS#12 keystore files. Security libraries will then translate from the keystore format to the JWK format before signing the JWT.

Assertion Validation and Public Keys

When the Authorization Server receives a JWT client assertion, it needs to first get a trusted public key to cryptographically verify the JWT's digital signature. A basic option is to copy in the full JWK asymmetric public key into the OAuth client configured in the Authorization Server.

The preferred option, with better management capabilities, is to instead provide public keys via a runtime download of a JSON Web Key Set (JWKS). This is a JSON payload containing one or more public keys in JWK format, along with key identifiers in kid fields:

{
  "keys": [
    {
      "kty": "RSA",
      "n": "yd88zkcXm6LmBe8Cd9GzpMxb9cM_nB3OW7g...",
      "e": "AQAB",
      "alg": "RS256",
      "kid": "1"
    }
  ]
}

When dynamic client registration is used, the public key details are provided in the registration request for the dynamic client being created. For further details see the Client Metadata section of the DCR specification.

JWT Validation with a JWKS URI

The JWKS is usually returned from a JWKS URI which is a URL provided by a trusted party. It is configured in the Authorization Server, for one or more OAuth clients. Incoming JWTs are then validated by the Authorization Server, which downloads public keys from the JWKS URI.

Validating received client assertions involves reading the kid field from the JWT header, then extracting the matching public key from the JWKS. The public key is then used to validate the JWT, after which the sub, iss, aud, jti and exp claims are also verified.

Assertion Verification

The JWKS URI can be provided by either the API owner, a trusted third party or a trusted client. It is straightforward to implement a JWKS URI within a simple REST API, which might read JWK public keys from a database. The endpoint exposes only public information so does not need securing.

Key Renewal

Whenever designing solutions using cryptographic keys, it is essential to also design a key renewal policy. It must be possible to issue new keys to clients periodically, then retire the old keys within an agreed time period.

The OpenID Connect specification describes how to deal with Signing Key Rotation. This mechanism is used by Authorization Servers, which must also periodically update their token signing keys. When a new key is added, it is used to sign future JWT access and ID tokens. When all old tokens are expired, the old signing key can be removed.

If hosting your own JWKS URI you can follow the same principles by providing admin operations on a REST API, to simply update the database containing public keys at the appropriate times. New keys should always be assigned different kid values, and for a while the JWKS will return both old and new public keys together.

Code Example

Once the design patterns are understood, an end-to-end solution for protecting data via client assertions requires only simple code. The API Secured by JWT Assertions code example walks through a Node.js implementation using the Curity Identity Server.

Conclusion

Client assertions are part of your security toolbox and enable you to design easy to manage strong security solutions for protecting data. Public key cryptography is used to identify clients, and message level security is used to avoid the need for special infrastructure. Solutions are JSON based and can be built in any technology stack, with a JWT library and minimal code. It remains important to review threats carefully, and to design a process for distributing keys to clients and renewing them.