Typescript Node.js guide for JWT signing and verifying using asymmetric keys

Intro

In modern applications there should be a way to identify authenticity of someone accessing various resources. It could be done by connecting to some oAuth provider like here with OneDrive. Or even implement own oAuth provider, but this would require some more efforts. Current guide will show how asymmetric keys approach could be used to issue signed JWT token. After this, signed token would be used to to authenticate in sample application without verifying token with issuer each time. This would significantly reduce load on issuer and makes token as proven user identity whatever it would be used.

Impatient section

Code available at https://gitlab.com/omakoleg/nodejs-jwt

yarn scripts available:

  • gen - generate keys into files
  • sign - sign sample data payload and store as token.txt
  • verify - verify signature of token.txt
  • jwks-server - JWKS backend
  • jwks-client-server - Sample protected backend
  • make-request - Access protected backend using token.txt

Sections

This guide consists of separate parts which could be used for particular use case.

  1. Asymmetric Keys generation

  2. Signing sample token payload with private key

  3. Verifying token signature using public key

  4. Accessing protected backend using token.

    a. JWKS endpoint provider backend

    b. Backend with protected resource

    c. Client requesting protected resource and using token for authentication

Common

With some scripts would be used shared values from this variables:

export const passphrase = 'some-secret-phrase';
export const kid = 'my-key-id';
export const issuer = 'my-issuer-name';

(1) Asymmetric Keys generation

generate

As a result of this section would be generated Asymmetric RSA keys

The same as explained on www.ssh.com and help.github.com

For current use case Node.js crypto module would be used.

gen.ts:

import { generateKeyPairSync } from 'crypto';
import { join } from 'path';
import { writeFileSync } from 'fs';
import { passphrase } from './config';

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
    modulusLength: 4096,
    publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
    },
    privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem',
        cipher: 'aes-256-cbc',
        passphrase
    }
});
writeFileSync(join('keys', '.private.key'), privateKey);
writeFileSync(join('keys', '.public.key.pem'), publicKey);

Most of code is from official docs

Here, using crypto.generateKeyPairSync keys pair generated, with passphrase and stored into files:

yarn gen sample Output:

$ yarn gen
yarn run v1.19.1
tsc && node gen.js
Private and Public Keys are generated
✨  Done in 2.57s.

(2) Signing sample token payload with private key

sign token

After generating Keys, they could be used to sign some payload. To not invent own signing and verifying approaches JWT token would be used. This format became some kind of default over time for most common use cases of web access authentication.

Data payload would consist of only one field: userId: 'a-b-c-d'. But more fields could added easily, and it worth to remember that token is passed from client to backend in request headers of cookies. So put inside only necessary data, like UserIds, roles, some basic user data like name, gender etc.

Never put inside private information !!!

Values in token are Signed, NOT Encrypted. Potentially anyone could decode them easily. You could also do check what is inside of any JWT token on main jwt.io page.

With help of jsonwebtoken Npm module creating JWT tokens in Node.js is easy task.

sign.ts:

import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import * as  jwt from 'jsonwebtoken';
import { passphrase, issuer, kid, JwtDataPayload } from './config';

const payload: JwtDataPayload = {
    userId: 'a-b-c-d'
}
const privateKey = readFileSync(join('keys', '.private.key'));
const token = jwt.sign(payload, {
    key: privateKey,
    passphrase
}, {
    algorithm: 'RS256',
    expiresIn: '1 day',
    issuer,
    keyid: kid
});
writeFileSync(join('keys', 'token.txt'), token);

In this script payload contents would be provided as signed JWT token. First, private key is loaded from file keys/.private.key. jwt.sign called with first parameter representing data to be included in token, second - private key and if needed passphrase, third - options for JWT token generation. From multiple available options only few would be used: algorithm, expiresIn, issuer and keyid.

expiresIn would define token lifetime. When token issued by somebody there is no way to revoke it easily, and actually it should not be done. So making short-lifetime tokens are in interests of issuer. As an example in case of token leak, it would be valid only for short time. Also with even shorter expiration, issuer could control authenticated session lifetime.

issuer would be used to verify that validated token was generated by somebody, who is expected

keyid is identifier of private key, used to sign JWT token. When multiple public keys available, this field would point to correct public key to be used.

Signed JWT token stored into file:

It would be easier to read it from file, rather than copy-paste always.

yarn sign sample output:

$ yarn sign
yarn run v1.19.1
tsc && node sign.js
Signed token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15LWtleS1pZCJ9.eyJ1c2VySWQiOiJhLWItYy1kIiwiaWF0IjoxNTc3Nzk4NjU5LCJleHAiOjE1Nzc4ODUwNTksImlzcyI6Im15LWlzc3Vlci1uYW1lIn0.FoMbaZQ_QunnBzt13dp23ujRO02H7qubrRCxVG88iTKp6LLeGdsDsSkOG8yQqQ2HoTmrwJseS72PHhNJlMkozx5w9af8fZrkzbihNsrwuZT8VOCVFeSZ9zfMZTmctO5jI8URrvmlparqEvJsh81fNYRy7qoFcbjy30-ZFKsHyK19DQJOLzwW6PqiWUi1yStLCw5DZtgdcQgSsBjr1DGkd8fzgkcje9HKhRvjl78zlyt7Px7e2e9StIdQO8ffomUHZFvhr4AtkdNBUu_tz1DFkTAwKAA8QGtleimgN6jv_p-sRdegTQepF80aCgGjkFzpcARRvHNFbwNFRJ7mut4MpsNBtpensS3QKmutAOL-mADjseF1Giu43LVNULKfAixRtS5hZwTyynFHmMrmYlvShjigI6nB2ozS0qLi86Vs0JxtT66zGfQkuxFtPXHyiQPaAi9mdQXrhUHvbfgr7WVFqEGtVXe3rAodpDOomrtec17EQhx1nuJy3lEMoAWJU3T8mTOTvUNZvI7AGOwSHxLiva7Bq3g1W7zgsGd_CoDjU-xIAuHMW4aQo3NwFf-DQncTHfSr4gRL85mKoeyMMkAFrDgXSoqdgmT9owjMehPC5CdKqC9wppMi_zRch0aLjllkf4hZGlH9U0pXxNR_snab24m1fItyDIHU8cIxMgLs2nk
✨  Done in 2.42s.

(3) Verifying token signature using public key

script verify

JWT token could verified online using UI at jwt.io.

Sample:

manual verify

Where:

A - generated token (also in keys/token.txt)

B - Decoded token fields. Includes userId

C - (optional) Public key used to sign token. From keys/.public.key.pem

D - (when C is set) Label to show that token was signed by provided public key

All those steps would be automated with help of already used jsonwebtoken.

verify.ts:

import { readFileSync } from 'fs';
import { join } from 'path';
import * as  jwt from 'jsonwebtoken';
import { issuer } from './config';

const token = readFileSync(join('keys', 'token.txt')).toString();
const publicKey = readFileSync(join('keys', '.public.key.pem'));
const decodedToken = jwt.verify(token, publicKey, {
    issuer,
    algorithms: ['RS256'],
    maxAge: '1 day'
});
console.log(`Decoded token data`, decodedToken);

Script is pretty simple. It load token (from keys/token.txt) and public key (from keys/.public.key.pem), then pass them into jwt.verify with options. Options are the same already used in second step Signing sample token of current guide.

Running yarn verify would give such output:

$ yarn verify
yarn run v1.19.1
tsc && node verify.js
Decoded token data { userId: 'a-b-c-d',
  iat: 1577800440,
  exp: 1577886840,
  iss: 'my-issuer-name' }
✨  Done in 2.36s.

In case of something went wrong, such errors could be thrown:

TokenExpiredError: jwt expired
JsonWebTokenError: invalid signature

In this case ensure yarn sign was executed recently, also issuer, algorithms and maxAge has correct values

(4) Accessing protected backend using token

Some real life example.

JWKS verifying

a. JWKS endpoint provider backend, who has Access to public key

b. Backend with protected resource,

c. Client requesting protected resource and using token for authentication

(4.a) JWKS endpoint provider backend

Backend would be express server running on port 3344. It would expose one API endpoint /keys. Payload is JSON Web Key Set (read more in RFC).

Based on RFC those fields marked as optional:

“kid” (Key ID) Parameter

“use” (Public Key Use) Parameter

But they would be needed later.

jwks-server.ts:

import express from 'express';
import { readFile } from 'fs';
import { join } from 'path';
import { kid } from './config';

// not typed
var pem2jwk = require('pem-jwk').pem2jwk;
const app = express()
app.get('/keys', (req, res) => {
    readFile(join('keys', '.public.key.pem'), (err, pem) => {
        if (err) { return res.status(500); }
        const jwk = pem2jwk(pem);
        res.json({
            keys: [{
                ...jwk,
                kid,
                use: 'sig'
            }]
        });
    });
})
app.listen(3344, () => console.log(`JWKS API is on port 3344`))

Using pem-jwk module previously generated public key in PEM format keys/.public.key.pem would be loaded and converted into jwk format. Before returning results, response would be given top level keys property and kid with use parameters added.

Run backend API:

$ yarn jwks-server       
yarn run v1.19.1
tsc && node jwks-server.js
JWKS API is on port 3344

In another terminal do request to /keys:

curl http://localhost:3344/keys |jq
  ...
{
  "keys": [
    {
      "kty": "RSA",
      "n": "r-kk12zYIKzr5veiczVsJjleoQQWdMJVoDpP0kDUwcHm...",
      "e": "AQAB",
      "kid": "my-key-id",
      "use": "sig"
    }
  ]
}

This API endpoint would be used in step (4.b).

(4.b) Backend with protected resource

For backend would be again used express running on port 2233. It would expose one API endpoint /with-auth.

jwks-client-server.ts:

import express, { Request, Response } from 'express';
import { issuer } from './config';
import jwt from 'express-jwt';
import jwksRsa from 'jwks-rsa';

const checkJwt = jwt({
    secret: jwksRsa.expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `http://localhost:3344/keys`
    }),
    issuer,
    algorithms: ['RS256'],
    requestProperty: 'tokenData'
});

const app = express()
app.get('/with-auth', checkJwt, (req: Request, res: Response) => {
    res.send(`Result: ${JSON.stringify(req.tokenData)}`);
})

app.listen(2233, () => console.log(`Backend is running on port 2233`))

For path handler action would be used middleware provided by jwks-rsa. And for secret loading jwks-rsa. For expressJwtSecret method, jwksUri parameter is required in would point to JWKS endpoint provided in step 4.b : http://localhost:3344/keys.

Within jwt function, few options are also used: issuer and algorithms. Those parameters would be used to verify token identity. After token verification and decoding it’s payload would be attached to express Request in a field with name provided in requestProperty.

To be able to use it with typescript, type definitions should be added.

Typings added to ./config.ts in the end of the file:

declare module 'express' {
    interface Request {
        tokenData?: JwtDataPayload
    }
}

As an API response would returned req.tokenData value.

Run Sample backend in another terminal:

$ yarn jwks-client-server
yarn run v1.19.1
tsc && node jwks-client-server.js
Backend is running on port 2233

Backend would be used in step 4.c

(4.c) Client requesting protected resource

As a prerequisite, backends from steps 4.a and 4.b should be Running:

In first terminal: yarn jwks-server

In second terminal: yarn jwks-client-server

In third terminal make test request with yarn make-request

$ yarn make-request
yarn run v1.19.1
tsc && node make-request.js
Result: {"userId":"a-b-c-d","iat":1577800440,"exp":1577886840,"iss":"my-issuer-name"}
✨  Done in 2.58s.

Script in make-request.ts:

import request from "request";
import { readFileSync } from 'fs';
import { join } from 'path';

const token = readFileSync(join('keys', 'token.txt')).toString();
var options = {
    method: 'GET',
    url: 'http://localhost:2233/with-auth',
    headers: { authorization: `Bearer ${token}` }
};
request(options, function (error, response, body) {
    if (error) throw new Error(error);
    console.log(body);
});

Would load signed key from keys/token.txt and provide it as Bearer Authentication Header to protected backend.

NPM packages

In this guide all functionality were provided by those NPM modules:

express - Backend server

jsonwebtoken - JWT signing and verifying functions

request - make sample client request

express-jwt - express middleware for jsonwebtoken

jwks-rsa - JWKS keys loader

pem-jwk - Converter to create JWKS key format from Pem public certificate

More options and provided methods could be checked on official pages for ech module

Conclusion

To secure application access nowadays exists multiple ways. Most common one is to use JWT token containing user identity identifier after logging in. JWT token when provided to the backend, could on it’s own ensure user identity (with signature validation of course). JWT support in Node.js is pretty good and straightforward and opens possibilities to quickly setting up token signing and validation logic.

This guide gave high level overview of JWT signing and verifying with asymmetric keys, and how it could be used together with JWKS provider in microservices architectures.

Once more, code available at https://gitlab.com/omakoleg/nodejs-jwt