28

How JWT works — the implementation | Medium

 3 years ago
source link: https://medium.com/@alexcambose/how-jwt-works-in-depth-354cb5dc360d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Image for post
Image for post

How JWT works? The implementation

Let’s build a NodeJS library that creates, signs, and verifies JWT tokens

How JWT works — the implementation

Why and how it works? Let’s build a NodeJS library that creates, signs, and verifies JWT tokens.

In this article, we will focus on building a simple JWT library that handles signing and verifying a JSON Web Token.

What is a JSON Web Token (JWT)?

JSON Web Token (JWT) is an open standard (RFC 7519) for securely transmitting information between endpoints as a JSON object.

A JWT consists of three main components, Header, Payload and signature, each separated by a dot. header.payload.signature We will briefly cover each of these different parts.

Header

The header typically specifies the signing algorithm and the type of the token.

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

alg - identifies which algorithm is used to generate the signature, HS256 indicates that this token is signed using HMAC-SHA256. typ - refers to the token type

This JSON gets stringified and then base64url encoded.

const header = {
"alg": "HS256",
"typ": "JWT"
};const encodedHeader = base64url.encode(JSON.stringify(header));

Payload

The second part is the payload of the token which contains a set of claims and any metadata required by your application to identify the user.

{
"name": "John",
"iat": 1591024013,
"exp": 1892023997
}

This example has the standard Issued At Time claim (iat), Expiration Time claim (exp) and a custom claim name. The payload object is then stringified and base64Url encoded to form the second part of the JWT.

const payload = {
"name": "John",
"iat": 1591024013,
"exp": 1892023997
};
const encodedPayload = base64url.encode(JSON.stringify(payload));

Signature

The third and final part of the JWT is the signature. It is composed by taking the encoded header, the encoded payload, a secret string, and applying the algorithm specified in the header.

If we use the HMAC SHA256 algorithm the signature will be created in the following way:

HMACSHA256(encodedHeader + "." + encodedPayload, secret)

By adding a signature to our token string, the ownership and integrity of the provided token can be verified.

Let’s build a simple Node.JS library

If you want a quick peek at the final result, you can check out this GitHub repo.

Firstly we need to create a new NodeJS project.

npm init -y

Once that’s done, you should have a package.json ready to go in your directory. This means you can start installing the dependencies you need for your project.

We will need Lodash and Base64 modules.

npm i -S base64url lodash

Our entry

Let’s start by creating an index.js file that will export our two main functions, verify and sign.

// index.js
const verify = require('./verify');
const sign = require('./sign');module.exports = {
verify,
sign,
};

The sign function

We will need a function that takes a payload, a secret, and some additional options, and returns a JWT.

// sign.js
module.exports = (payloadData, secret, options = {}) => {
};

Since the options object can contain both header and payload parameters/claims, we need to filter and add them only when necessary. For simplicity reasons, I’ll use the pick and omit functions from Lodash. As the name suggests, the _.pick(object, ['key1', 'key2', ...]) function will take an object and an array of keys, and will return a new object that contains only the keys specified in the array. The omit function is the opposite of pick. For more information please see the official documentation for pick and omit.

A starting point for the sign function

After setting up the header and payload, it’s time to see how we can generate a signature. In this guide, we’ll focus on generating the signature using the HMACSHA256 algorithm. This can be done using the native crypto module.

const crypto =  require('crypto');
// ...
const signature = crypto.createHmac('sha256', secret).update(`${encodedHeader}.${encodedPayload}`).digest('base64');

Some potential small improvements would be to automatically calculate the expiration date based on the number of seconds provided in the exp claim and to automatically set the header alg to none in case there is no secret specified.

For calculating the exp claim:

if (options.exp) {
options.exp += Math.round(new Date().getTime() / 1000);
}

And to automatically set the alg to none in case, there is no secret provided (and also return the token without the signature)

if (!secret || header.alg === 'none') {
header.alg = 'none';
const encodedHeader = base64url(JSON.stringify(header));return `${encodedHeader}.${encodedPayload}.`;
}

The final working code would look like this:

The final sign function

The verify function

The verify function needs to take a token and a secret and check if the signature is correct. Although not necessarily required, we can also base64Url decode the payload for further use.

The signature verification implies that, if we sign the given header and payload with the secret, the result will be the same as the provided signature in the token.

// recalculate the signature based on the header and payload
const signature = crypto.createHmac('sha256', secret).update(`${tokenHeader}.${tokenPayload}`).digest('base64');// the essential part, checking if the signatures match
if (base64url.fromBase64(signature) !== tokenSignature) {
throw new Error('Invaid signature');
}

I’ll point out again, the signature is basically a hash function that takes header + payload + secret as input and any change in one of these parts will totally alter the resulting hash.

Besides checking if the signature is correct and since we added the exp claim calculation, we can also check here if the token is still valid based on the current date.

// if the `exp` claim is set, verify if it's not expired
if (payload.exp && payload.exp < Date.now()) {
throw new Error('Expired token');
}

And the final verify function will look like this:

The verify function

Let’s try creating a token and then verifying it using our new functions.

Demo of signing and verifying a token

Please note that this implementation is far from a final one, I strongly recommend that you stick to an already build module since there are a lot more things to take into consideration when creating, signing and verifying a JWT. This article is meant to give you just a peek on how the core works.

Let’s build a simple NodeJS library

If you want a quick peek at the final result, you can check out this GitHub repo.

Firstly we need to create a new NodeJS project.

npm init -y

Once that’s done, you should have a package.json ready to go in your directory. This means you can start installing the dependencies you need for your project.

We will need Lodash and Base64 modules.

npm i -S base64url lodash

Our entry

Let’s start by creating an index.js file that will export our two main functions, verify and sign.

// index.js
const verify = require('./verify');
const sign = require('./sign');module.exports = {
verify,
sign,
};

The sign function

We will need a function that takes a payload, a secret, and some additional options, and returns a JWT.

// sign.js
module.exports = (payloadData, secret, options = {}) => {
};

Since the options object can contain both header and payload parameters/claims, we need to filter and add them only when necessary. For simplicity reasons, I’ll use the pick and omit functions from Lodash. As the name suggests, the _.pick(object, ['key1', 'key2', ...]) function will take an object and an array of keys, and will return a new object that contains only the keys specified in the array. The omit function is the opposite of pick. For more information please see the official documentation for pick and omit.

A starting point for the sign function

After setting up the header and payload, it’s time to see how we can generate a signature. In this guide, we’ll focus on generating the signature using the HMACSHA256 algorithm. This can be done using the native crypto module.

const crypto =  require('crypto');
// ...
const signature = crypto.createHmac('sha256', secret).update(`${encodedHeader}.${encodedPayload}`).digest('base64');

Some potential small improvements would be to automatically calculate the expiration date based on the number of seconds provided in the exp claim and to automatically set the header alg to none in case there is no secret specified.

For calculating the exp claim

if (options.exp) {
options.exp += Math.round(new Date().getTime() / 1000);
}

and to automatically set the alg to none in case, there is no secret provided (and also return the token without the signature)

if (!secret || header.alg === 'none') {
header.alg = 'none';
const encodedHeader = base64url(JSON.stringify(header));return `${encodedHeader}.${encodedPayload}.`;
}

The final working code would look like this:

The final sign function

The verify function

The verify function needs to take a token and a secret and check if the signature is correct. Although not necessarily required, we can also base64Url decode the payload for further use.

The signature verification implies that, if we sign the given header and payload with the secret, the result will be the same as the provided signature in the token.

// recalculate the signature based on the header and payload
const signature = crypto.createHmac('sha256', secret).update(`${tokenHeader}.${tokenPayload}`).digest('base64');// the essential part, checking if the signatures match
if (base64url.fromBase64(signature) !== tokenSignature) {
throw new Error('Invaid signature');
}

I’ll point out again, the signature is basically a hash function that takes header + payload + secret as input and any change in one of these parts will totally alter the resulting hash.

Besides checking if the signature is correct and since we added the exp claim calculation, we can also check here if the token is still valid based on the current date.

// if the `exp` claim is set, verify if it's not expired
if (payload.exp && payload.exp < Date.now()) {
throw new Error('Expired token');
}

And the final verify function will look like this:

The verify function

Let’s try creating a token and then verifying it using our new functions.

Demo of signing and verifying a token

Please note that this implementation is far from a final one, I strongly recommend that you stick to an already build module since there are a lot more things to take into consideration when creating, signing and verifying a JWT. This article is meant to give you just a peek on how the core works.

You can find the source code on GitHub.

Thanks for reading!

P.S. If you liked this article, it would mean a lot if you hit the recommend button or share with friends.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK