WangHansen / jwt-auth

JWT authentication library with built-in key rotation and token revocation function
https://github.com/WangHansen/jwt-auth
MIT License
17 stars 4 forks source link

Build Status codecov License: MIT FOSSA Status


JWT Auth

A light weight authentication library that supports key rotation and revokation list.

Table of Contents

Another auth library?

There are a lot of authentication libraries out there that deals with JWT, probably the most popular one(the one that I used a lot in my project) is the passport-jwt library used together with passport. However, the library has the few problems:

In order to address these problems, I decided to make this open source library.

Getting Started

Prerequisites

I have this tested from Node version 12 and above, make sure you have the right version

Installation

Install with npm

npm install --save @hansenw/jwt-auth

Usage

Simple Usage

// authService.js
import JWTAuth from "@hansenw/jwt-auth";

const JWT = new JWTAuth();
export default JWT;

// to use in other files
import jwt from "./authService";

// to generate a jwt token
const token = jwt.sign({ /* some payload */ });

// to verify
try {
  const payload = jwt.verify(token);
  // ...
} catch (e) {
  // cannot be verified
}

// to revoke
await jwt.revoke(token);
jwt.verify(token); // this will throw JWTRevoked

With Express

import * as express from "express";
import JWTAuth from "@hansenw/jwt-auth";

const app = express();
const jwt = new JWTAuth();

app.post("/login", async (req, res, next) => {
  const { username, password } = req.body;

  // Your own logic .. to verify credentials
  const match = authenticate(username, password);

  if (match) {
    const jwtpayload = { username };
    const token = jwt.sign(payload);
    res.set({
        "Access-Control-Expose-Headers": "Authorization",
        Authorization: "Bearer " + token,
      })
      .json({
        message: "Login success",
      });
  } else {
    // handle failure logic
  }
})

// middleware for protecting api
function authGuard(req, res, next) {
  // getting token from header
  const header = req.headers["authorization"];
  const token = header ? header.split(" ")[1] : "";
  if (!token) {
    return next(new Error("No auth token"));
  }
  // verify token validity
  try {
    const payload = jwt.verify(token);
    // if token is valid, attach the payload to req object
    req.payload = payload;
  } catch (e) {
    // token invalid, can be handled differently based on the error
  }
}

app.post("/protected", authGuard, async (req, res, next) => { 
  // get JWT payload
  const payload = req.payload;

  // if user info is ever needed
  const user = await db.collection("user").find({ username: payload.username });

  res.json({ message: "Authorized user only" })
})

// start the express app
app.listen(3000)

Advanced Usage with TS

Customze what to store in the revocation list, be default revocation list contain items on type { jti: string, exp: number }

import JWTAuth, { RevocationListItem } from "@hansenw/jwt-auth";

interface RevocListItem extends RevocationListItem {
  ip: string;
}

const jwt = new JWTAuth<RevocListItem>();

const token = jwt.sign({ /* some payload */ });

// to verify
try {
  const payload = jwt.verify(token);
  // ...
} catch (e) {
  // cannot be verified
}

// to revoke
await jwt.revoke(token, (payload) => ({
  jti: payload.jti,
  exp: payload.exp,
  ip: req.ip,
}));
jwt.verify(token); // this will throw JWTRevoked

Microservice

If you want to build your own auth server or auth service within the microservices, check out this jwt-jwks-client library I made that can be used together with this one.

server.ts

import * as express from "express";
import JWTAuth from "@hansenw/jwt-auth";

const app = express()
const authService = new JwtAuth();

app.post("/login", (req: Request, res: Response) => {
  // Replace with your own matching logic
  if (req.body.username === "admin" && req.body.password === "password") {
    const token = authService.sign({ userId: "admin" });
    return (
      res
        .set("authorization", token)
        .send("Authorized")
    );
  }
  res.status(401).send("Not authorized");
});

// Expose jwks through an API
app.get("/jwks", (req: Request, res: Response) => {
  res.json(authService.JWKS(true));
});

Client

import * as express from "express";
import JwksClient from "jwt-jwks-client";

const authClient = new JwksClient({
  jwksUri: "http://localhost:3000/jwks",
  secure: false,
});

app.get("/secret", async (req: Request, res: Response) => {
  const token = req.headers.authorization;
  if (token) {
    // Verify the token here
    await authClient.verify(token);
    return res.send("This is a secret page");
  }
  return res.send(`You are not authorized to see the secret page`);
});

See complete example here

API

Constructor

Class: JWTAuth

const jwt = new JWTAuth(options: JwtAuthOptions);

JwtAuthOptions:

Methods

jwt.sign(payload: object, options?: JWT.SignOptions)

Generate a new jwt token

const token = jwt.sign(payload, options?);

jwt.verify(token: string, options?: JWT.SignOptions) throws

Verify the validity of a JWT token

try {
  const payload = jwt.verify(token, options?);
} catch (error) {
  // possible error: JWTClaimInvalid, JWTExpired, JWTMalformed, JWTRevoked
}

jwt.revoke(token: string, revocListHandler?: Function) throws

Revoke an already issued JWT token

try {
  await jwt.revoke(token)
} catch (error) {
  // failed to revoke a token
}

jwt.JWKS(isPrivate: boolean)

Return all keys in the format of JWKS

const jwks = jwt.JWKS();

jwt.rotate()

Rotate the key sets by removing the oldest key and generating a new key

await jwt.rotate();

jwt.revokeKey(kid: string)

Manually remove a key from key set, Note: this may cause all the JWT signed with this kid invalid

await jwt.revokeKey(kid);

jwt.reset()

Remove all current keys and generate a new set, Note: !!this will cause all the JWT signed previously invalid

await jwt.reset();

Persistent Storage

It is important to save the generated keys to a persistent storage, so that application crashes and restart would not result in all authenticated users log out

File Storage

By default, this library comes with one storage plugin--local file storage, this storage tries to store all the data in a folder on local disk.

Since the keys are very sensitive and secretive data, I don't think it is safe to send them on to the internet, thus I only provide the file storage so that it can be securely stored. If you want to store it with databases, please see write your own persisten plugin

import JwtAuth, { FileStorage } from "@hansenw/jwt-auth";

const jwtAuth = new JwtAuth();

const fileStore = new FileStorage();

// this is async beacuse it will try to load keys from storage
await jwtAuth.setStorage(fileStore);

// after the storage is set, every time key rotation happens
// keys will be automatically saved to file storage
await jwtAuth.rotate();

API

Constructor

Class: FileStorage

const jwt = new FileStorage(options: FileStorageConfig);

FileStorageConfig:

Write your own persistent storage

Make sure you extend the Storage abstract provided by the library

abstract class Storage<T extends RevocationListItem> {
  abstract loadKeys(): Promise<JSONWebKeySet | undefined>;
  abstract saveKeys(keys: JSONWebKeySet): Promise<void>;
  abstract loadRevocationList(): Promise<Array<T> | undefined>;
  abstract saveRevocationList(list: Array<T>): Promise<void>;
}

All you need is to provide 4 methods for storing and retriving data from the persistent storage

Methods

All methods on Storage will be called automatically with the auth library so you do't need to worry about calling them

Here is the list of actions that happens with auth library assuming the storage is used:

Auth library method Storage method
rotate saveKeys
revokeKey saveKeys
reset saveKeys
revoke saveRevocList

Roadmap

Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

Distributed under the MIT License. See LICENSE for more information.

FOSSA Status