How to manually validate a JWT signature using online tools

It's all a matter of formats and encoding.

On https://jwt.io you get this token based on your input values and secret:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

We want to prove that the signature:

3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

is correct.

The signature is a HMAC-SHA256 hash that is Base64url encoded. (as described in RFC7515)

When you use the online HMAC generator to calculate a hash for

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

with the secret

hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6

you get

de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3

as result, which is a HMAC-SHA256 value, but not Base64url encoded. This hash is a hexadecimal string representation of a large number.

To compare it with the value from https://jwt.io you need to convert the value from it's hexadecimal string representation back to a number and Base64url encode it.

The following script is doing that and also uses crypto-js to calculate it's own hash. This can also be a way for you to verify without JWT libraries.

var CryptoJS = require("crypto-js");

// the input values
var base64Header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
var base64Payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
var secret = "hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6";

// two hashes from different online tools
var signatureJWTIO = "3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M";
var onlineCaluclatedHS256 =  "de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3";

// hash calculation with Crypto-JS. 
// The two replace expressions convert Base64 to Base64url format by replacing 
// '+' with '-', '/' with '_' and stripping the '=' padding
var base64Signature = CryptoJS.HmacSHA256(base64Header + "." + base64Payload , secret).toString(CryptoJS.enc.Base64).replace(/\+/g,'-').replace(/\//g,'_').replace(/\=+$/m,'');

// converting the online calculated value to Base64 representation
var base64hash = new Buffer.from(onlineCaluclatedHS256, 'hex').toString('base64').replace(/\//g,'_').replace(/\+/g,'-').replace(/\=+$/m,'')


// the results:
console.log("Signature from JWT.IO             : " + signatureJWTIO);
console.log("NodeJS calculated hash            : " + base64Signature);
console.log("online calulated hash (converted) : " + base64hash);

The results are:

Signature from JWT.IO             : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

NodeJS calculated hash            : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

online calulated hash (converted) : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

identical!

Conclusion:

The values calculated by the different online tools are all correct but not directly comparable due to different formats and encodings. A little script as shown above might be a better solution.


I had the same problem until I figured out that I was using plain base64 encoding instead of base64url. There are also some minor details in between. Here is the step-by-step manual that will, hopefully, make the whole process much more clear.

Notes

Note 1: You must remove all spaces and newlines from your JSON strings (header and payload). It is implicitly done on jwt.io when you generate a JWT token.

Note 2: To convert JSON string to base64url string on cryptii.com create the following configuration:

First view: Text

Second view: Encode
    Encoding: Base64
    Variant: Standard 'base64url' (RFC 4648 §5)

Third view: Text

Note 3: To convert HMAC HEX code (signature) to base64url string on cryptii.com create the following configuration:

First view: Bytes
    Format: Hexadecimal
    Group by: None

Second view: Encode
    Encoding: Base64
    Variant: Standard 'base64url' (RFC 4648 §5)

Third view: Text

Manual

You are going to need only two online tools:

  1. [Tool 1]: cryptii.com - for base64url encoding,
  2. [Tool 2]: codebeautify.org - for HMAC calculation.

On cryptii.com you can do both base64url encoding/decoding and also HMAC calculation, but for HMAC you need to provide a HEX key which is different from the input on jwt.io, so I used a separate service for HMAC calculation.

Input data

In this manual I used the following data:

  • Header:

    {"alg":"HS256","typ":"JWT"}
    
  • Payload:

    {"sub":"1234567890","name":"John Doe","iat":1516239022}
    
  • Secret (key):

    The Earth is flat!
    

The secret is not base64 encoded.

Step 1: Convert header [Tool 1]

  • Header (plain text):

    {"alg":"HS256","typ":"JWT"}
    
  • Header (base64url encoded):

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    

Step 2: Convert payload [Tool 1]

  • Payload (plain text):

    {"sub":"1234567890","name":"John Doe","iat":1516239022}
    
  • Payload (base64url encoded):

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
    

Step 3: Calculate HMAC code (signature) [Tool 2]

Calculate HMAC using SHA256 algorithm.

  • Input string (base64url encoded header and payload, concatenated with a dot):

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
    
  • Calculated code (HEX number):

    c8a9ae59f3d64564364a864d22490cc666c74c66a3822be04a9a9287a707b352
    

The calculated HMAC code is a HEX representation of the signature. Note: it should not be encoded to base64url as a plain text string but as a sequence of bytes.

Step 4: Encode calculated HMAC code to base64url [Tool 1]:

  • Signature (Bytes):

    c8a9ae59f3d64564364a864d22490cc666c74c66a3822be04a9a9287a707b352
    
  • Signature (base64url encoded):

    yKmuWfPWRWQ2SoZNIkkMxmbHTGajgivgSpqSh6cHs1I
    

Summary

Here are our results (all base64url encoded):

  • Header:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    
  • Payload:

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
    
  • Signature:

    yKmuWfPWRWQ2SoZNIkkMxmbHTGajgivgSpqSh6cHs1I
    

The results from jwt.io:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.yKmuWfPWRWQ2SoZNIkkMxmbHTGajgivgSpqSh6cHs1I

As you can see, all three parts are identical.

Tags:

Jwt