How to utilize 3rd party libraries like nimbus-jose and jwt.io to use x5t fingerprint to verify and validate JWT created by WSO2 API Manager

Sampath Rajapakse
7 min readFeb 13, 2019

--

First, Let’s see what is a JWT and the use of x5t thumbprint.

What is a JSON Web Token?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. [1]

Although JWTs can be encrypted(JWE) to also provide secrecy between parties, WSO2 has focused on signed tokens(JWS). Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it. [1]

Structure of a APIM generated JWT

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

Therefore, a JWT typically looks like the following.

xxxxx.yyyyy.zzzzz

Let’s break down the different parts.

Header

The header consists of three parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA and x5t (X.509 Certificate SHA-256 Thumbprint) which is a hexified base64url-encoded SHA-256 thumbprint (a.k.a. digest)of the DER encoding of the X.509 certificate(public key).

For example:

{
"typ": "JWT",
"alg": "RS256",
"x5t": "NTAxZmMxNDMyZDg3MTU1ZGM0MzEzODJhZWI4NDNlZDU1OGFkNjFiMQ"
}

Payload

The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.

For example:

{
"aud": "http://org.wso2.apimgt/gateway",
"sub": "Sampath",
"application": {
"id": 3,
"name": "Test JWT",
"tier": "Unlimited",
"owner": "Sampath"
},
"scope": "am_application_scope default",
"iss": "https://localhost:9443/oauth2/token",
"keytype": "PRODUCTION",
"subscribedAPIs": [],
"consumerKey": "FPGkgJ6wP0fV1KMKqdEo7_U5dSEa",
"exp": 1549542345,
"iat": 1549538745278,
"jti": "c058831a-7c84–40da-a1d8–85db4a6c68d3"
}

Here, APIM includes “jti” claim in order to allow JWT revocation. The header and payload is then Base64Url encoded to form the first and second parts of the JSON Web Token.

Signature

To create the signature part you have to take the encoded header, the encoded payload, public key or certificate, the algorithm specified in the header, and sign that.

For example APIM use the RSASHA256 algorithm, the signature will be created in the following way:

RSASHA256( 
base64UrlEncode(header) + "." + base64UrlEncode(payload),
public key or certificate).

Finalized JWT

The output is three Base64-URL encoded strings separated by dots that can be easily passed in HTML and HTTP environments, while being more compact when compared to XML-based standards such as SAML.

The following shows a JWT that has the previous header and payload encoded, and it is signed with a certificate.

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5UQXhabU14TkRNeVpEZzNNVFUxWkdNME16RXpPREpoWldJNE5ETmxaRFUxT0dGa05qRmlNUSJ9.eyJhdWQiOiJodHRwOlwvXC9vcmcud3NvMi5hcGltZ3RcL2dhdGV3YXkiLCJzdWIiOiJTYW1wYXRoIiwiYXBwbGljYXRpb24iOnsiaWQiOjMsIm5hbWUiOiJUZXN0IEpXVCIsInRpZXIiOiJVbmxpbWl0ZWQiLCJvd25lciI6IlNhbXBhdGgifSwic2NvcGUiOiJhbV9hcHBsaWNhdGlvbl9zY29wZSBkZWZhdWx0IiwiaXNzIjoiaHR0cHM6XC9cL2xvY2FsaG9zdDo5NDQzXC9vYXV0aDJcL3Rva2VuIiwia2V5dHlwZSI6IlBST0RVQ1RJT04iLCJzdWJzY3JpYmVkQVBJcyI6W10sImNvbnN1bWVyS2V5IjoiRlBHa2dKNndQMGZWMUtNS3FkRW83X1U1ZFNFYSIsImV4cCI6MTU0OTU0MjM0NSwiaWF0IjoxNTQ5NTM4NzQ1Mjc4LCJqdGkiOiJjMDU4ODMxYS03Yzg0LTQwZGEtYTFkOC04NWRiNGE2YzY4ZDMifQ==.g3CZ6PfirbN9rUHDhWxXNbqA49ne2Fam_SXWiKUBfQN10ddLIKrY8L8xalQ0DX3LiQg0TmdYspR4vVd3Mfji919UJEFSe894JD-PqFTFcvVsLgLm9pWNoyXNVEz6gTiR5JM_UlAPZ4N8FjCla9tMjRfiG-l4gvdmXhF84_a7IwwVLY7DkOs0m-7UK3evB005QZJ5_vmYEtAFV6pFzSt3Jz_fqEPR4RIkrjUIei7m4KyQ3QEl-_ZuhPn80UrACbFqyIQ0Fl71cMQ5V28AXKt8AitGT53XL0LK-cnNmQqyy8WTNRaWmVAZIW7XOCcHBkg9diWy-uZvHSL3pig9Pfr0sg==

Usage of x5t thumbprint

A thumbprint is a digest of the whole certificate. In this case APIM uses the SHA1 algorithm. Sometimes applications ask for its fingerprint, which easier for work with, instead of requiring the X.509 public certificates (a long string).

X.509 public certificate APIM uses:

-----BEGIN CERTIFICATE-----
MIIDSTCCAjGgAwIBAgIEAoLQ/TANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJV
UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxDTALBgNVBAoT
BFdTTzIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA3MTkwNjUyNTFaFw0yNzA3
MTcwNjUyNTFaMFUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
TW91bnRhaW4gVmlldzENMAsGA1UEChMEV1NPMjESMBAGA1UEAxMJbG9jYWxob3N0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAluZFdW1ynitztkWLC6xK
egbRWxky+5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V
9+PmjdGtau4CTXu9pLLcqnruaczoSdvBYA3lS9a7zgFU0+s6kMl2EhB+rk7gXluE
ep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP4HxXyq
K9neolXI9fYyHOYILVNZ69z/73OOVhkh/mvTmWZLM7GM6sApmyLX6OXUp8z0pkY+
vT/9+zRxxQs7GurC4/C1nK3rI/0ySUgGEafO1atNjYmlFN+M3tZX6nEcA6g94Iav
yQIDAQABoyEwHzAdBgNVHQ4EFgQUtS8kIYxQ8UVvVrZSdgyide9OHxUwDQYJKoZI
hvcNAQELBQADggEBABfk5mqsVUrpFCYTZZhOxTRRpGXqoW1G05bOxHxs42Paxw8r
AJ06Pty9jqM1CgRPpqvZa2lPQBQqZrHkdDE06q4NG0DqMH8NT+tNkXBe9YTre3EJ
CSfsvswtLVDZ7GDvTHKojJjQvdVCzRj6XH5Truwefb4BJz9APtnlyJIvjHk1hdoz
qyOniVZd0QOxLAbcdt946chNdQvCm6aUOputp8Xogr0KBnEy3U8es2cAfNZaEkPU
8Va5bU6Xjny8zGQnXCXxPKp7sMpgO93nPBt/liX1qfyXM7xEotWoxmm6HZx8oWQ8
U5aiXjZ5RKDWCCq4ZuXl6wVsUz1iE61suO5yWi8=
-----END CERTIFICATE-----

when this certificate is hashed with sha1, following hexified strings are generated.

Thumbprint:
501fc1432d87155dc431382aeb843ed558ad61b1

Formatted Thumbprint: 50:1F:C1:43:2D:87:15:5D:C4:31:38:2A:EB:84:3E:D5:58:AD:61:B1

After generating thumbprint base64url encode the hexified string to generate x5t value.

x5t:
NTAxZmMxNDMyZDg3MTU1ZGM0MzEzODJhZWI4NDNlZDU1OGFkNjFiMQ==

If you look closely, the x5t in the header is little different from the generated x5t due to generated x5t having two trailing “=” characters. This omission of trailing “=” characters is permitted in the section 3.2 of JWS Spec. Therefore, verification and validation of JWT can be done without any issues.

JWT Token Generation

Follow the API Manager documentation to generate a JWT token. Simplified steps are as follows.

  1. Download WSO2 API Manager from WSO2 website.
  2. Enable JWT generation configuration in JWT implementation in the <API-M_HOME>/repository/conf/api-manager.xml file. Here, you have to uncomment the relevant elements.
  3. Start WSO2 API Manager by going to the <API-M_HOME>/bin directory using the command-line and then executing wso2server.bat (for Windows) or wso2server.sh (for Linux and Mac)
  4. Sign in to the API Store (https://<hostname>:9443/store) with the admin/admin credentials.
  5. Create an application with JWT selected as Token Type.
  6. Go to Production Keys tab and click Generate Keys to generate a JWT token.

Now, you have a basic understanding of a JWT token , x5t thumbprint and how to generate a JWT token from WSO2 Api Manager.

Certificate / Public Key Generation

Before verifying or validating the JWT we need to have certificate/ public key. Let’s see how to generate the public key.

  1. Navigate to <API-M_HOME>/repository/resources/security/wso2carbon.jks
    and use the following command to convert jks to p12 format.
keytool -importkeystore -srckeystore wso2carbon.jks -destkeystore myfile.p12 -srcstoretype JKS - deststoretype PKCS12 -deststorepass wso2carbon

2. Use the following command to convert the created new myfile.p12 certificate to pem format.

openssl pkcs12 -clcerts -nokeys -out myKey.pem -in myfile.p12

When prompted for a password, use wso2carbon as the password. Created pem file should be as follows.

Bag Attributes
friendlyName: wso2carbon
localKeyID: 54 69 6D 65 20 31 35 34 39 34 34 31 34 32 30 39 30 32
subject=/C=US/ST=CA/L=Mountain View/O=WSO2/CN=localhost
issuer=/C=US/ST=CA/L=Mountain View/O=WSO2/CN=localhost
-----BEGIN CERTIFICATE-----
MIIDSTCCAjGgAwIBAgIEAoLQ/TANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJV
UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxDTALBgNVBAoT
BFdTTzIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA3MTkwNjUyNTFaFw0yNzA3
MTcwNjUyNTFaMFUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
TW91bnRhaW4gVmlldzENMAsGA1UEChMEV1NPMjESMBAGA1UEAxMJbG9jYWxob3N0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAluZFdW1ynitztkWLC6xK
egbRWxky+5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V
9+PmjdGtau4CTXu9pLLcqnruaczoSdvBYA3lS9a7zgFU0+s6kMl2EhB+rk7gXluE
ep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP4HxXyq
K9neolXI9fYyHOYILVNZ69z/73OOVhkh/mvTmWZLM7GM6sApmyLX6OXUp8z0pkY+
vT/9+zRxxQs7GurC4/C1nK3rI/0ySUgGEafO1atNjYmlFN+M3tZX6nEcA6g94Iav
yQIDAQABoyEwHzAdBgNVHQ4EFgQUtS8kIYxQ8UVvVrZSdgyide9OHxUwDQYJKoZI
hvcNAQELBQADggEBABfk5mqsVUrpFCYTZZhOxTRRpGXqoW1G05bOxHxs42Paxw8r
AJ06Pty9jqM1CgRPpqvZa2lPQBQqZrHkdDE06q4NG0DqMH8NT+tNkXBe9YTre3EJ
CSfsvswtLVDZ7GDvTHKojJjQvdVCzRj6XH5Truwefb4BJz9APtnlyJIvjHk1hdoz
qyOniVZd0QOxLAbcdt946chNdQvCm6aUOputp8Xogr0KBnEy3U8es2cAfNZaEkPU
8Va5bU6Xjny8zGQnXCXxPKp7sMpgO93nPBt/liX1qfyXM7xEotWoxmm6HZx8oWQ8
U5aiXjZ5RKDWCCq4ZuXl6wVsUz1iE61suO5yWi8=
-----END CERTIFICATE-----

Now we have a newly created JWT token and a public key / certificate to verify and validate that JWT token.

Signature Verification With jwt.io

This is the easiest way to verify a JWT. You simply have to go to jwt.io website and paste the JWT on left hand side Encoded plane and put the certificate/ public key on the Verify Signature text box provided.

Here, only the signature is verified which means it only checks whether the header and payload of the JWT hasn’t been changed by an intruder. This tool doesn’t check claims inside the payload. For example checking whether the token hasn’t expired by using “exp” claim.

Finally, let’s look at a library which provide more thoruogh verification and validation of the JWT.

Validation with maven: com.nimbusds / nimbus-jose-jwt / 6.0

First let’s see what are the features and algorithms supported by this library.

jose-jwt library features
  1. Create a Maven project named JoseTest by adding the following dependency to the pom configuration.
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>6.0</version>
</dependency>

2. Add a main class named App.java and add main class path to the pom configuration.

<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.sampath.App</mainClass>
</manifest>
</archive>
</configuration>

3. Add following code to the App.java main class.

package com.sampath;

import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.*;
import com.nimbusds.jwt.proc.*;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
/**
* JWT Validation Main Class
*
*/

public class App
{
public static void main( String[] args )
{

// The access token to validate, typically submitted with a HTTP header like
// Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InMxIn0.eyJzY3A...

String accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5UQXhabU14TkRNeVpEZzNNVFUxWkdNME16RXpPREpoWldJNE5ETmxaRFUxT0dGa05qRmlNUSJ9.eyJhdWQiOiJodHRwOlwvXC9vcmcud3NvMi5hcGltZ3RcL2dhdGV3YXkiLCJzdWIiOiJhZG1pbiIsImFwcGxpY2F0aW9uIjp7ImlkIjo1LCJuYW1lIjoiVGVzdCIsInRpZXIiOiJVbmxpbWl0ZWQiLCJvd25lciI6ImFkbWluIn0sInNjb3BlIjoiYW1fYXBwbGljYXRpb25fc2NvcGUgZGVmYXVsdCIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImtleXR5cGUiOiJQUk9EVUNUSU9OIiwic3Vic2NyaWJlZEFQSXMiOltdLCJjb25zdW1lcktleSI6IlgxaDdGZmR5dlpNc1VGcFBVa2N4ZlA0d1V1d2EiLCJleHAiOjE1NDk4OTA0MDgsImlhdCI6MTU0OTg4NjgwODY1MiwianRpIjoiYzcwYTNjZjEtNTE1ZC00Nzg2LWFlMWEtNmM3ZDk0MjdiZWYxIn0=.LavQBIPpaF28M29SVjCJSiLj7NcsRbHPvXF8gq9Kf7dNsVYvI1v4r1FDYofkytLlPvDUxQz6AhhxZnXzgEsghiG8akskqh5LdBJZrvqFbuYWQhqpiBYgFlRsOdASXKE76lg6Odb_VguABpDc5X2jZhH4s1iAEH5Bgh_lbxA19tUPGLYvH1mb4VY0zU0ah-Rbe9RmHN1MyPCEwyWwhd7yCmplqYX8buSWnbnweprn0PtCpbB9xzokx9nvKXu6mF2n3VM1bX0Re-qWuuJ8jMyQ8FXUUuSf6DR1YEfdoq2at-pcKL0IuavZPIWMLIcclpT26KRDZkaAUhcg_727HmV73g==";
// Set up a JWT processor to parse the tokens and then check their signature
// and validity time window (bounded by the "iat", "nbf" and "exp" claims)
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();

// The public RSA keys to validate the signatures will be sourced from the
// OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet
// object caches the retrieved keys to speed up subsequent look-ups and can
// also gracefully handle key-rollover
JWKSource keySource = null;
try {
keySource = new RemoteJWKSet(new URL("https://localhost:9443/oauth2/jwks"));

} catch (MalformedURLException e) {
e.printStackTrace();
}

// The expected JWS algorithm of the access tokens (agreed out-of-band)
JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;

// Configure the JWT processor with a key selector to feed matching public
// RSA keys sourced from the JWK set URL
//JWKSource keySourceNew = "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"26e3d9c9-7f28-4e83-9eca-9593e9a0653d\",\"n\":\"luZFdW1ynitztkWLC6xKegbRWxky-5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V9-PmjdGtau4CTXu9pLLcqnruaczoSdvBYA3lS9a7zgFU0-s6kMl2EhB-rk7gXluEep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP4HxXyqK9neolXI9fYyHOYILVNZ69z_73OOVhkh_mvTmWZLM7GM6sApmyLX6OXUp8z0pkY-vT_9-zRxxQs7GurC4_C1nK3rI_0ySUgGEafO1atNjYmlFN-M3tZX6nEcA6g94IavyQ\"}";
JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
jwtProcessor.setJWSKeySelector(keySelector);

// Process the token
SecurityContext ctx = null; // optional context parameter, not required here
JWTClaimsSet claimsSet = null;
try {
claimsSet = jwtProcessor.process(accessToken, ctx);
} catch (ParseException e) {
e.printStackTrace();
} catch (BadJOSEException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}

// Print out the token claims set
System.out.println(claimsSet.toJSONObject());
}
}

Here, the generated JWT is the value represented by the accessToken variable. This library uses JWKS to verify and validate the provided JWT. This JWKS remote set is provided by the https://localhost:9443/oauth2/jwks endpoint by the API Manager. Following figure shows the json payload coming from the jwks endpoint.

{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"NTAxZmMxNDMyZDg3MTU1ZGM0MzEzODJhZWI4NDNlZDU1OGFkNjFiMQ","alg":"RS256","n":"luZFdW1ynitztkWLC6xKegbRWxky-5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V9-PmjdGtau4CTXu9pLLcqnruaczoSdvBYA3lS9a7zgFU0-s6kMl2EhB-rk7gXluEep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP4HxXyqK9neolXI9fYyHOYILVNZ69z_73OOVhkh_mvTmWZLM7GM6sApmyLX6OXUp8z0pkY-vT_9-zRxxQs7GurC4_C1nK3rI_0ySUgGEafO1atNjYmlFN-M3tZX6nEcA6g94IavyQ"}]}

“kid” claim here represents the same x5t certificate value and “n” claim represents public key.

4. Before running JoseTest maven project you need to run API Manager since Jose library need to access jwks endpoint in API Manager to select keys to verify and validate the JWT.

5. Run the JoseTest maven project by building and executing generated jar with the following command.

java - jar jose-test-1.0-SNAPSHOT.jar

6. If all went well, the console output should be something like the following which gives the claims of the provided JWT/

{"aud":"http:\/\/org.wso2.apimgt\/gateway","sub":"admin","application":{"owner":"admin","tier":"Unlimited","name":"JWT","id":4},"scope":"am_application_scope default","iss":"https:\/\/localhost:9443\/oauth2\/token","keytype":"PRODUCTION","subscribedAPIs":[],"consumerKey":"dsSz97526ETK8WmNAArZ2S4p4u8a","exp":1550080724,"iat":1550077124,"jti":"db40fd48-550d-468b-9d92-7a555936c913"}

7. The output will be something like below if the JWT is expired before testing.

com.nimbusds.jwt.proc.BadJWTException: Expired JWT
at com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier.<clinit>(DefaultJWTClaimsVerifier.java:62)
at com.nimbusds.jwt.proc.DefaultJWTProcessor.<init>(DefaultJWTProcessor.java:139)
at com.sampath.App.main(App.java:36)
Exception in thread "main" java.lang.NullPointerException
at com.sampath.App.main(App.java:73)

Process finished with exit code 1

Here, jose library will match x5t thumbprint coming from the JWKS endpoint with the value from the matched key set. matched key set is created using the values coming from the JWT header. Following is the code responsible to create the matched key set which can be seen inside the Jose library.

return new JWKMatcher.Builder()
.keyType(KeyType.forAlgorithm(getExpectedJWSAlgorithm()))
.keyID(jwsHeader.getKeyID())
.keyUses(KeyUse.SIGNATURE, null)
.algorithms(getExpectedJWSAlgorithm(), null)
.x509CertSHA256Thumbprint(jwsHeader.getX509CertSHA256Thumbprint())
.build();

Hope, you have gained some insight into JWT and verifying and validation procedures.

Sources:

[1]https://jwt.io/
[2]https://tools.ietf.org/html/rfc7515
[3]https://docs.wso2.com/display/AM260/Passing+Enduser+Attributes+to+the+Backend+Using+JWT

--

--