Self-contained JWTs

Self-contained JWTs

architect

Overview

This article will explain some alternative ways in which APIs can validate JWT access tokens, and the related use cases. The general pattern is to include additional key information in the JWT header, which the API can then read and supply to a JWT validation library.

Standard JWT Validation

The most common option for an API is to receive a JWT header with fields similar to the following:

{ 
    kid: '359322535',
    alg: 'RS256'
}

The API then makes a request to the JWKS Endpoint of the Authorization Server over a trusted SSL connection, after which the key material received is trusted. The response provides JSON web keys (JWK) as described in RFC7517, and the below example payload is for the widely used RS256 algorithm:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "359322535",
      "use": "sig",
      "alg": "RS256",
      "n": "0gPWzqkjREVjJa9w6qjhYkijDGR ...",
      "e": "AQAB",
      "x5t": "tQ_s8QFqkpcuzWXTvsWHocNqiOQ"
    }
  ]
}

The API then looks up the key that matches the received kid field and supplies it to the JWT library, which derives the public key from the n (modulo) and e (exponent) fields. The JWT library then uses the public key to verify the signature of the received JWT, to ensure it is cryptographically correct and has not been tampered with.

Once JWT validation is complete, the API caches the JWK and uses the cached value for future API requests whose JWT header has the same kid field. This avoids excessive calls to the JWKS endpoint, which could otherwise lead to performance problems.

Alternative JWT Use Cases

There are a couple of important scenarios where the standard option may not meet your requirements, and these are the most common:

Use CaseBehavior
Serverless / Cloud NativeIn some API deployment scenarios a new instance of the API is spun up on every API request, or the API must remain stateless, so that calling the JWKS endpoint per request is prohibitive in performance terms
Financial GradeIt is possible for APIs to make additional trust checks against tokens using a Public Key Infrastructure (PKI), and in some setups this can ensure that trust brokering relationships between parties are verified

JWT Header Fields

Your Authorization Server should enable you to include additional fields so that token signing and trust details are made available in a Self-contained manner. As an example, the Curity Identity Server enables you to configure one or more Token Issuers, where these options can be activated. In the following sections we will drill into how the following fields are used when validating JWTs:

Header FieldDescription
x5tThis is a SHA-1 hash of an X.509 certificate. This “thumbprint” of the certificate is why this header is referred to as an x5t for short.
x5t#s256This is a SHA-256 hash of an X.509 certificate , and preferred over x5t since it is less likely to result in collisions.
x5cThis is also related to X.509 certificates, but the “C” in this header refers to the chain of certificates of the one used to sign the JWT.
jwkA JSON web key provides token signing details and can also convey trust information via x5t, x5t#s256 and x5c fields.

x5t and x5t#s256

JWTs can be validated by deploying one or more token signing certificates with the API, and then loading the certificate that matches the received x5t or x5t#s256 value. In this setup the API has everything it needs locally, but the JWT is not really Self-contained since it does not contain the public key used for validation.

Once loaded, the certificate is then used to validate the JWT in the standard way. The API does not check in with the Authorization Server however, so is unable by default to detect JWTs signed with revoked token signing keys.

x5c

The x5c field can be included in the JWT header, to provide a Self-contained JWT with token signing certificate details. The format of this field is described in detail in RFC7515, and it can optionally include a full trust chain, where the token signing certificate itself is the first entry:

{
  kid: '-1614245140',
  x5t: 'lDdNIsb3FxulMcYdAXxYJ_Z5950',
  x5c: [
    'MIIDqjCCApKgAwIBAgIESLNEvDA ...',
    'MIICwzCCAasCCQCKVy9eKjvi+jA ...',
    'MIIDTDCCAjSgAwIBAgIJAPlnQYH...'
  ],
  alg: 'RS256'
}

Using the x5c field to validate tokens verifies the full trust chain of the token using a Public Key Infrastructure, and can support sophisticated trust brokering setups. Private PKI setups are common these days, in which case the x5c verification proves that the JWT was signed with a private key issued by the company.

The PKI verification can include revocation checks to detect compromised token signing keys, so that those tokens are not accepted by the API. This can include the use of a Certificate Revocation List (CRL) or Online Certificate Status Protocol (OCSP) responder, to provide revoked key details.

jwk

This option is typically used in scenarios where the x5c certificate chain only contains certificate authorities and not the token signing certificate itself. The public key is then provided in the JWT header as a jwk object, which can optionally include trust information in its x5c field:

{
  jwk: {
    kty: 'RSA',
    alg: 'RSA',
    n: 'hLjc255V6xkm3zBhh1tlTj14B ...',
    e: 'AQAB',
    x5c: [
      'MIIDqjCCApKgAwIBAgIESLNEvDA ...',
      'MIICwzCCAasCCQCKVy9eKjvi+jA ...',
      'MIIDTDCCAjSgAwIBAgIJAPlnQYH...'
    ],
    x5t: 'lDdNIsb3FxulMcYdAXxYJ_Z5950'
  },
  alg: 'RS256'
}

When relying on a JWK to verify a JWT you must ensure that it was not issued by a malicious party, and there are two alternative mechanisms here:

  • Using OpenID Connect Discovery, to verify that the JWT’s iss claim maps to an https URL whose certificate chain the API trusts
  • Using PKI to verify x5c certificates included with the JWK, which many companies consider more established, and which avoids the need for the API to make any https calls

Validating Self-contained JWTs

When using Self-contained JWTs, an additional step is required, so that an attacker cannot create a JWT with their own certificate and send it to your API:

StepDescription
Verify the Trust ChainEnsure that the received token signing public key is provided by a trusted issuer, as detailed in RFC5280
Standard JWT ValidationThen verify the digital signature of the JWT, and also check the expiry, issuer and audience

Certificate Trust Chains

The trust chain of the token signing certificate may look like this, where the CA certificates are long lived and can be deployed with your API, as a trust store:

CertificateRoleLifetime
Signing CertificateUsed to digitally sign the JWT6 months
Intermediate CAA certificate authority used to issue the token signing certificate10 years
Root CAA certificate authority used to issue the intermediate certificate10 years

In this type of setup the API will not have to be concerned with key management issues for 10 years or so. It also never has to make any outgoing calls, to get data from endpoints such as JWKS, Metadata or Webfinger endpoints. This provides a very simple, safe, and maintainable setup.

Integrating with JWT Libraries

The high level steps to validate a Self-contained JWT should not require much code, since security libraries will do the main work, as in the following example Javascript code:

const receivedCertChain = getCertChain(x5c);
const trustStore = getWhitelistedCAs();
verifyTrustChain(receivedCertChain, trustStore);

const tokenSigningPublicKey = createKey(receivedCertChain[0]);
const options = {
    algorithms: ['RS256'],
    issuer: 'https://login.example.com/oauth/v2/oauth-anonymous',
    audience: 'api.example.com',
};
verifyJwt(accessTokenJwt, tokenSigningPublicKey, options);

JWT validation libraries usually support supplying the token signing details in a variety of ways, so that validating a Self-contained JWT does not require much code:

OptionMechanism
As a JWKS downloadAPI supplies a JWKS endpoint to the library
As a JWKAPI supplies a jwk object to the library
As a CertificateAPI supplies the first certificate from the x5c array to the library

Video

This video provides further information on architectural choices around APIs and JWTs:

Conclusion

Self-contained JWTs are useful in APIs that cannot cache JSON web keys, and they can also be used to make additional X509 trust checks, such as in financial grade APIs or advanced trust brokering setups. For a fully worked example on verifying JWTs containing x5c, jwk or x5t fields, see the Serverless API Code Example.

Keep up with our latest articles and how-tos RSS feeds.