Sign & Verify JWT (HMAC SHA256) in Deno

Mayank C
Tech Tonic

--

This article is a follow-up of the earlier article on JWT. Deno’s web crypto APIs have evolved, therefore an update is needed. The earlier article is still useful to get an introduction to JWT.

Introduction

At times, applications need to sign & send data, then receive & verify the signature of a piece of data. For example — JWT (JSON web token). The JWT is a base64 URL encoded data appended with a signature that ensures its integrity when it sent back to the application for authentication. A great introduction to JWT is here.

A very commonly used signature/verification algorithm is HMAC SHA-256. There are others, but HMAC SHA-256 is perhaps the most popular one. Most of the JWT applications use it.

In this article, we’ll go over how to create & verify a signature for a piece of data. JWT is a common application of this.

Basic usage

Imports

All the basic (generate/import key, sign, verify) functions are present in Deno’s core runtime under crypto.subtle. As JWTs use base64 URL formatting, an import of standard library’s base URL encoding/decoding module is required.

import {encode, decode} from "https://deno.land/std/encoding/base64url.ts";

Step 1 — Key

The HMAC SHA-256 algorithm needs a secret for signing & verifying (can also be called key). The secret/key can either be generated or imported. The output of generateKey/importKey function is a key object that can be used to sign & verify.

Generate key

To generate a random key, generateKey function need to be used. A detailed overview of the function is here. Here is a common usage:

const key = await crypto.subtle.generateKey(
{name: "HMAC", hash: "SHA-256"},
false, //extractable
["sign", "verify"] //uses
);

Let’s go over the common inputs quickly:

  • Name: Must always be HMAC
  • Hash: The hashing algorithm (could be SHA-1, SHA-256, SHA-384, and SHA-512)
  • extractable: true means key can be exported
  • Uses: The uses of key, sign & verify

As mentioned above, there are many algorithms that can be used to generate key. We’ve kept the scope limited to HMAC SHA256.

Import key

In large-scale production grade web services, the distributed applications would get a key from some other service/database (like a keystore service, etc.). In these cases, the key needs to be imported (instead of generated). A detailed overview of importKey function is here. We’ll go over the common usage only.

const rawKey=new TextEncoder().encode("013d3270-b0a0-46f8-9e56-2265ba768e12");
const key = await crypto.subtle.importKey(
"raw",
rawKey,
{ name: "HMAC", hash: "SHA-256" },
false, //extractable
["sign", "verify"], //uses
);

Let’s go over the common inputs quickly:

  • raw: Key is in raw format (no structure)
  • rawKey: The raw key data
  • name: Must always be HMAC
  • Hash: The hashing algorithm (could be SHA-1, SHA-256, SHA-384, and SHA-512)
  • extractable: true means key can be exported
  • Uses: The uses of key, sign & verify

Step 2 — Sign

To generate a signature, the sign function need to be used. A detailed overview of the sign function is here. The sign function is a general purpose signing function that works on any kind of data. We’ll see the common usage of signing a JWT payload. The output of the sign function is the signature. The basic usage of sign function is:

const signature = await crypto.subtle.sign(
{name: "HMAC"},
key,
new Uint8Array(1000).fill(65) //data
);

Let’s over the common inputs quickly:

  • name: Must always be HMAC
  • key: The signing key (either generated or imported)
  • data: The data to sign

Step 3 — Verify

To verify a received signature, the verify function need to be used. A detailed overview of the verify function is here. We’ll see the common usage of verifying the signature received in the JWT. The output of verify function is boolean: true (if signature is valid), false (otherwise). The basic usage of verify function is:

const result = await crypto.subtle.verify(
{name: "HMAC"},
key,
signature,
new Uint8Array(1000).fill(65) //data
);

Let’s go over the inputs quickly:

  • Name: This must be HMAC
  • Key: The signing key
  • Signature: The received signature that needs to be verified
  • Data: The data that was signed

Create & Verify JWT

To know more about JWT structure, visit the article here.

In short, JWT consists of three parts:

  • Header: Algorithm, etc.
  • Payload: The application specific data
  • Signature: The signature is computed over header+payload

All the three parts of JWT are base64 URL encoded & delimited by period ‘.’:

JWT=base64URLEncode(header)+’.’+base64URLEncode(payload)+’.’+base64URLEncode(signature)

The following is the complete code to import key, sign, and verify a JWT.

import {encode as bEnc, decode as bDec} from "https://deno.land/std/encoding/base64url.ts";const tEnc=(d:string)=>new TextEncoder().encode(d), tDec=(d:Uint8Array)=>new TextDecoder().decode(d);const genKey=async (k:string)=>await crypto.subtle.importKey("raw", tEnc(k), {name:"HMAC", hash:"SHA-256"}, false, ["sign", "verify"]);const getJWT=async (key:CryptoKey, data:any)=>{
const payload=bEnc(tEnc(JSON.stringify({alg:"HS256", typ:"JWT"})))+'.'+bEnc(tEnc(JSON.stringify(data)));
const signature=bEnc(new Uint8Array(await crypto.subtle.sign({name:"HMAC"}, key, tEnc(payload))));
return `${payload}.${signature}`;
};
const checkJWT=async (key:CryptoKey, jwt:string)=>{
const jwtParts=jwt.split(".");
if(jwtParts.length!==3) return;
const data=tEnc(jwtParts[0]+'.'+jwtParts[1]);
if(await crypto.subtle.verify({name:"HMAC"}, key, bDec(jwtParts[2]), data)===true)
return JSON.parse(tDec(bDec(jwtParts[1])));
};

That’s all the code! ~5 lines of application code to generate & verify JWT

Here is a code using above functions to import key, create, and verify JWT:

const data={exp: Date.now(), a: 'b', c: 'd', e: 100};
const key=await genKey("013d3270-b0a0-46f8-9e56-2265ba768e12");
const jwt=await getJWT(key, data);
//jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzAyNTc4NzY1MTMsImEiOiJiIiwiYyI6ImQiLCJlIjoxMDB9.2EFOA5-sa_taJEEVDaP_xKSA-Nv5IqQYj_-MhmpG1J8
const data=await checkJWT(key, jwt);
//data: { exp: 1630257876513, a: "b", c: "d", e: 100 }

--

--