JWT Overview

  • Overview
  • Decode with browser JavaScript
    • Check expiration
  • Decode with keypairs CLI
  • Example

A JWT (JSON Web Token) is a “compressed” JWS (JSON Web Signature) with three Base64-encoded parts:

JWT/JWS Component Description
(Protected) Header contains metadata related to verification, such as the Key ID (kid) and signature algorithm (alg)
(Claims) Payload data describing the user, account, bot, etc and related access grants
Signature the RSA, ECDSA, HMAC (shared secret), or other signature of <protected>.<payload> (note that ECDSA signatures will be different even when encoding the same data)

Decoding or Parsing a JWT is simply a matter of converting from the “compressed” Base64-encoded form that looks like this:

xxxx.yyyy.zzzz

to the “decompressed” form - known as JWS - that looks like this:

{
    "protected": "<base64-encoded-headers>",
    "header": { "typ": "JWT", "kid": "..." },
    "payload": "<base64-encoded-claims>",
    "claims": { "iss": "https://example.com", "sub": "..." },
    "signature": "<base64-encoded-signature>"
}

Since the ordering of JSON is not strictly deterministic (the keys may be listed alphabetically, or by parse order, or seemingly random), the base64-encoded versions of “header” (called “protected”) and “claims” (called “payload”) must be generated for signing and preserved for verification.

Decode JWT via CLI

keypairs is a command line tool for generating, decoding, and verifying JWTs:

keypairs inspect 'xxxx.yyyy.zzzz'

JWT Parser in JavaScript

Here’s 30 lines of vanilla JS for decoding a JWT:

Usage:

// Parse
let jws = parseJwt("xxxx.yyyy.zzzz");

// Check expiration
let secondsSinceEpoch = Math.ceil(Date.now() / 1000)
let fresh = jws.claims.exp > secondsSinceEpoch;

Source:

function parseJwt(jwt) {
  let parts = jwt.split(".");
  let jws = {
    protected: parts[0],
    payload: parts[1],
    signature: parts[2],
  };
  jws.header = urlBase64ToJson(jws.protected);
  jws.claims = urlBase64ToJson(jws.payload);

  return jws;
};

// Ironically, JavaScript's native Base64 implementation isn't URL-safe. :)
function urlBase64ToBase64(str) {
  let r = str % 4;
  if (2 === r) {
    str += "==";
  } else if (3 === r) {
    str += "=";
  }
  return str.replace(/-/g, "+").replace(/_/g, "/");
}

function urlBase64ToJson(u64) {
  let b64 = urlBase64ToBase64(u64);
  let str = atob(b64);
  return JSON.parse(str);
}

License:

/**
 * @license
 * parse-jwt.js - decode / parse / decompress a JWT into a JWS (plus JSON)
 *
 * Written in 2020 by AJ ONeal <coolaj86@gmail.com>
 * To the extent possible under law, the author(s) have dedicated all copyright
 * and related and neighboring rights to this software to the public domain
 * worldwide. This software is distributed without any warranty.
 *
 * You should have received a copy of the CC0 Public Domain Dedication along with
 * this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
 */

Example of how to Decode a JWT

Here’s a more complete example of what a JWT (compressed JWS) looks like:

eyJhbGciOiJFUzI1NiIsImtpZCI6Ik5nWmlZZ0xCeDZVZ09zU25RYnZqYlMzSS01OGdwajVHOVlvdXZOMGN3U2MiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsiY2hhbGxlbmdlOmVtYWlsIl0sImF1dGhfdGltZSI6MTY1NTc2MTU3MCwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjMzMTE3Njc5NTMsImdpdmVuX25hbWUiOiJNZSIsImlhdCI6MTY1NTc2MTU3MCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImp0aSI6IlJuREJDUVJtVUJraDNtOEsyYnJlSFcifQ.mbUjGeKnScKfqiGuQM85EcKZtb0ONLaRDnXWwJ0GMKAHQKJGERPg0mnk9Vf6sSdmYZYc0CTCgBS92K_Wlw0PuA
  1. Split the token string on . (period) to produce the component parts of the JWS:
    let jwt = "xxxx.yyyy.zzzz";
    
    let [protected, payload, signature] = token.split('.');
    
    let jws = {
      protected: protected,
      payload: payload,
      signature: signature,
    }
    
    # The Protected Header
    eyJhbGciOiJFUzI1NiIsImtpZCI6Ik5nWmlZZ0xCeDZVZ09zU25RYnZqYlMzSS01OGdwajVHOVlvdXZOMGN3U2MiLCJ0eXAiOiJKV1QifQ
    
    # The Claims Payload
    eyJhbXIiOlsiY2hhbGxlbmdlOmVtYWlsIl0sImF1dGhfdGltZSI6MTY1NTc2MTU3MCwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjMzMTE3Njc5NTMsImdpdmVuX25hbWUiOiJNZSIsImlhdCI6MTY1NTc2MTU3MCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImp0aSI6IlJuREJDUVJtVUJraDNtOEsyYnJlSFcifQ
    
    # The Signature - the result of sign(protected + "." + payload)
    mbUjGeKnScKfqiGuQM85EcKZtb0ONLaRDnXWwJ0GMKAHQKJGERPg0mnk9Vf6sSdmYZYc0CTCgBS92K_Wlw0PuA
    
  2. Decode the url-safe base64 into JSON:
    jws.header = JSON.parse( decodeUrlBase64( jws.payload ) );
    jws.claims = JSON.parse( decodeUrlBase64( jws.payload ) );
    

Example of Parsed Result

And here’s a more complete example of a JWS (plus decoded JSON):

{
  "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik5nWmlZZ0xCeDZVZ09zU25RYnZqYlMzSS01OGdwajVHOVlvdXZOMGN3U2MiLCJ0eXAiOiJKV1QifQ",
  "header": {
    "alg": "ES256",
    "kid": "NgZiYgLBx6UgOsSnQbvjbS3I-58gpj5G9YouvN0cwSc",
    "typ": "JWT"
  },
  "payload": "eyJhbXIiOlsiY2hhbGxlbmdlOmVtYWlsIl0sImF1dGhfdGltZSI6MTY1NTc2MTU3MCwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjMzMTE3Njc5NTMsImdpdmVuX25hbWUiOiJNZSIsImlhdCI6MTY1NTc2MTU3MCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImp0aSI6IlJuREJDUVJtVUJraDNtOEsyYnJlSFcifQ",
  "claims": {
    "amr": [
      "challenge:email"
    ],
    "auth_time": 1655761570,
    "email": "me@example.com",
    "email_verified": true,
    "exp": 3311767953,
    "given_name": "Me",
    "iat": 1655761570,
    "iss": "https://example.com",
    "jti": "RnDBCQRmUBkh3m8K2breHW"
  },
  "signature": "mbUjGeKnScKfqiGuQM85EcKZtb0ONLaRDnXWwJ0GMKAHQKJGERPg0mnk9Vf6sSdmYZYc0CTCgBS92K_Wlw0PuA"
}

The description of the various headers and claims - such as amr, auth_time, and jti - can be found in the OpenID Connect Core 1.0 incorporating errata set 1 under Section 5.1 Standard Claims:

See also