express-jwt vs express-jwt-authz vs jsonwebtoken vs jwt-simple vs koa-jwt vs passport-jwt
Architecting JWT Authentication in Node.js Backends
express-jwtexpress-jwt-authzjsonwebtokenjwt-simplekoa-jwtpassport-jwtSimilar Packages:

Architecting JWT Authentication in Node.js Backends

This comparison evaluates the leading Node.js libraries for implementing JSON Web Token (JWT) authentication. The ecosystem splits into three distinct categories: low-level cryptographic utilities (jsonwebtoken, jwt-simple), framework-specific middleware (express-jwt, koa-jwt), and strategy-based integrations (passport-jwt). While jsonwebtoken remains the industry standard for creating and verifying tokens, middleware like express-jwt and koa-jwt automate the extraction and validation process for their respective frameworks. passport-jwt offers a flexible strategy pattern suitable for complex auth flows, whereas jwt-simple is deprecated and should be avoided in modern applications due to security limitations.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express-jwt04,51428.5 kB642 years agoMIT
express-jwt-authz0987.75 kB10-MIT
jsonwebtoken018,19043.4 kB2089 months agoMIT
jwt-simple01,357-347 years agoMIT
koa-jwt01,35043.2 kB7-MIT
passport-jwt01,97852 kB42-MIT

Architecting JWT Authentication in Node.js: A Deep Dive into Libraries

Securing Node.js applications with JSON Web Tokens (JWT) is a standard practice, but the ecosystem offers multiple ways to implement it. Choosing the wrong library can lead to security gaps, excessive boilerplate, or tight coupling to a specific framework. This analysis breaks down the six most common packages, distinguishing between low-level crypto tools, framework middleware, and strategic wrappers.

🔐 Core Cryptography: Building vs. Using Tokens

Before protecting routes, you must be able to create and verify tokens. This is where the low-level libraries come in.

jsonwebtoken is the de facto standard for Node.js. It supports all modern algorithms (RS256, ES256, HS256) and provides a robust API for signing and verifying.

// jsonwebtoken: Signing and verifying manually
const jwt = require('jsonwebtoken');
const secret = 'my-secret';

// Create a token
const token = jwt.sign({ userId: 123, role: 'admin' }, secret, { expiresIn: '1h' });

// Verify a token
try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded.userId); // 123
} catch (err) {
  console.error('Invalid token');
}

jwt-simple was once popular for its minimal API but is now deprecated. It lacks support for many modern algorithms and has not been maintained in years. Using it exposes your application to potential cryptographic weaknesses.

// jwt-simple: Deprecated approach (DO NOT USE)
const jwt = require('jwt-simple');
const secret = 'my-secret';

// Creating a token (limited algorithm support)
const token = jwt.encode({ userId: 123 }, secret);

// Verifying (risky in production)
try {
  const decoded = jwt.decode(token, secret);
} catch (err) {
  // Error handling is less robust than jsonwebtoken
}

⚠️ Critical Warning: jwt-simple is officially deprecated. Never use it for new projects. Always prefer jsonwebtoken for cryptographic operations.

🚦 Framework Middleware: Automatic Verification

In typical API development, you don't want to manually call jwt.verify in every route handler. Middleware packages automate this by checking the Authorization header, verifying the token, and attaching the user data to the request object.

express-jwt is the standard middleware for Express. It automatically validates the token and throws an error if invalid, or passes control to the next handler if valid.

// express-jwt: Automatic verification middleware
const expressJwt = require('express-jwt');
const app = require('express')();

// Protect all routes starting with /api
app.use('/api', expressJwt({ secret: 'my-secret', algorithms: ['HS256'] }) );

app.get('/api/profile', (req, res) => {
  // req.auth contains the decoded token payload
  res.json({ user: req.auth.userId });
});

// Error handler for invalid tokens
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    res.status(401).send('Invalid token');
  }
});

koa-jwt serves the same purpose for Koa applications but adapts to Koa's context (ctx) and async/await middleware pattern.

// koa-jwt: Automatic verification for Koa
const koaJwt = require('koa-jwt');
const Koa = require('koa');
const app = new Koa();

// Middleware protects downstream routes
app.use(koaJwt({ secret: 'my-secret', algorithms: ['HS256'] }).unless({ path: ['/login'] }));

app.use(async (ctx) => {
  // ctx.state.user contains the decoded payload
  ctx.body = { user: ctx.state.user.userId };
});

🛡️ Authorization: Checking Permissions

Verifying a token tells you who the user is. Sometimes you also need to check what they are allowed to do (scopes or permissions). This is where express-jwt-authz fits in.

express-jwt-authz is not a standalone verifier; it works alongside express-jwt. It checks specific claims (like scope) against a list of required permissions.

// express-jwt-authz: Scope-based authorization
const expressJwt = require('express-jwt');
const jwtAuthz = require('express-jwt-authz');
const app = require('express')();

// First verify identity, then check permissions
app.use(expressJwt({ secret: 'my-secret', algorithms: ['HS256'] }));

app.get('/api/admin', 
  jwtAuthz(['read:users', 'delete:users']), // Requires these scopes
  (req, res) => {
    res.send('Admin action allowed');
  }
);

🧩 Strategy Pattern: Flexible Integration

Sometimes you need more flexibility than simple middleware provides. You might need to extract tokens from cookies instead of headers, or support multiple authentication strategies.

passport-jwt implements the JWT strategy for the Passport.js framework. It requires a bit more setup but offers powerful customization for token extraction and verification.

// passport-jwt: Strategy-based integration
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

const options = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: 'my-secret',
  algorithms: ['HS256']
};

passport.use(new JwtStrategy(options, (jwtPayload, done) => {
  // You can lookup the user in your database here
  if (jwtPayload.userId) {
    return done(null, { id: jwtPayload.userId });
  }
  return done(null, false);
}));

// Usage in Express route
app.get('/api/dashboard', 
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ data: 'Secure content' });
  }
);

📊 Comparative Analysis

1. Security and Maintenance Status

Security is paramount in authentication. jsonwebtoken is actively maintained and supports the latest algorithms. express-jwt and koa-jwt rely on jsonwebtoken internally, inheriting its security posture (provided you specify algorithms explicitly). passport-jwt is also well-maintained within the Passport ecosystem. In stark contrast, jwt-simple is deprecated. It does not receive security patches and lacks support for critical algorithms like RS256, making it unsafe for production use.

2. Developer Experience and Boilerplate

If you want "zero config" protection for an Express API, express-jwt wins. It reduces five lines of verification logic to a single middleware call. koa-jwt offers similar DX for Koa users. However, if you need to customize how the token is retrieved (e.g., from a custom header X-Auth-Token), passport-jwt provides a cleaner configuration interface via ExtractJwt than trying to hack the extraction logic in middleware.

3. Flexibility vs. Convention

jsonwebtoken gives you 100% flexibility but requires you to write the wiring code yourself. express-jwt and koa-jwt follow a convention-over-configuration approach, which is faster but harder to deviate from if your auth flow is non-standard. passport-jwt sits in the middle: it enforces a strategy pattern but allows deep customization of that strategy.

📌 Summary Table

PackageTypeFrameworkMaintenanceBest Use Case
jsonwebtokenCrypto UtilityNone✅ ActiveCreating/verifying tokens manually
jwt-simpleCrypto UtilityNone❌ DeprecatedNone (Avoid completely)
express-jwtMiddlewareExpress✅ ActiveStandard Express API protection
koa-jwtMiddlewareKoa✅ ActiveStandard Koa API protection
express-jwt-authzAuthorizationExpress✅ ActiveChecking scopes/permissions
passport-jwtStrategyExpress/Any✅ ActiveComplex extraction or multi-strategy apps

💡 Final Architectural Recommendation

For most modern Node.js projects, your dependency tree should look like this:

  1. Core Dependency: Always install jsonwebtoken. Even if you use middleware, you may need it for issuing tokens in your login route.
  2. Framework Layer:
    • If using Express, add express-jwt for route protection. Add express-jwt-authz if you use scopes.
    • If using Koa, add koa-jwt.
    • If using Passport.js already, or need custom token extraction, use passport-jwt instead of the middleware packages.
  3. Avoid: Remove jwt-simple from any consideration. The minor convenience it once offered is not worth the security debt.

By selecting the right tool for your specific layer (crypto vs. middleware vs. strategy), you ensure your authentication flow is secure, maintainable, and aligned with your framework's design patterns.

How to Choose: express-jwt vs express-jwt-authz vs jsonwebtoken vs jwt-simple vs koa-jwt vs passport-jwt

  • express-jwt:

    Choose express-jwt if you are building an Express application and need automatic token verification attached to the request object. It is ideal for standard REST APIs where you want to protect routes with minimal boilerplate, but be aware that recent versions require explicit algorithms to prevent security vulnerabilities.

  • express-jwt-authz:

    Select express-jwt-authz specifically when you need to enforce scope-based or permission-based access control after the token has been validated. It pairs directly with express-jwt to check claims like scope or custom permissions before allowing a request to reach your controller logic.

  • jsonwebtoken:

    Use jsonwebtoken as your foundational dependency if you need full control over token creation, signing, and verification logic. It is the correct choice for custom authentication services, microservices that issue tokens, or when framework-specific middleware does not fit your architectural needs.

  • jwt-simple:

    Do NOT choose jwt-simple for any new project; it is deprecated and lacks critical security features found in modern libraries. Its simplicity comes at the cost of robust algorithm support and maintenance, making it a significant security risk compared to jsonwebtoken.

  • koa-jwt:

    Opt for koa-jwt if your backend is built on Koa.js, as it leverages Koa's middleware composition and context object naturally. It provides the same automatic verification benefits as express-jwt but is designed specifically for Koa's async/await flow and context handling.

  • passport-jwt:

    Pick passport-jwt if you are already using the Passport.js ecosystem or require a strategy that supports complex extraction methods (like custom header names or cookie extraction). It is best for applications that might need to swap authentication strategies later or support multiple auth mechanisms simultaneously.

README for express-jwt

express-jwt

This module provides Express middleware for validating JWTs (JSON Web Tokens) through the jsonwebtoken module. The decoded JWT payload is available on the request object.

Install

$ npm install express-jwt

API

expressjwt(options)

Options has the following parameters:

  • secret: jwt.Secret | GetVerificationKey (required): The secret as a string or a function to retrieve the secret.
  • getToken?: TokenGetter (optional): A function that receives the express Request and returns the token, by default it looks in the Authorization header.
  • isRevoked?: IsRevoked (optional): A function to verify if a token is revoked.
  • onExpired?: ExpirationHandler (optional): A function to handle expired tokens.
  • credentialsRequired?: boolean (optional): If its false, continue to the next middleware if the request does not contain a token instead of failing, defaults to true.
  • requestProperty?: string (optional): Name of the property in the request object where the payload is set. Default to req.auth.
  • Plus... all the options available in the jsonwebtoken verify function.

The available functions have the following interface:

  • GetVerificationKey = (req: express.Request, token: jwt.Jwt | undefined) => Promise<jwt.Secret>;
  • IsRevoked = (req: express.Request, token: jwt.Jwt | undefined) => Promise<boolean>;
  • TokenGetter = (req: express.Request) => string | Promise<string> | undefined;

Usage

Basic usage using an HS256 secret:

var { expressjwt: jwt } = require("express-jwt");
// or ES6
// import { expressjwt, ExpressJwtRequest } from "express-jwt";

app.get(
  "/protected",
  jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

The decoded JWT payload is available on the request via the auth property.

The default behavior of the module is to extract the JWT from the Authorization header as an OAuth2 Bearer token.

Required Parameters

The algorithms parameter is required to prevent potential downgrade attacks when providing third party libraries as secrets.

:warning: Do not mix symmetric and asymmetric (ie HS256/RS256) algorithms: Mixing algorithms without further validation can potentially result in downgrade vulnerabilities.

jwt({
  secret: "shhhhhhared-secret",
  algorithms: ["HS256"],
  //algorithms: ['RS256']
});

Additional Options

You can specify audience and/or issuer as well, which is highly recommended for security purposes:

jwt({
  secret: "shhhhhhared-secret",
  audience: "http://myapi/protected",
  issuer: "http://issuer",
  algorithms: ["HS256"],
});

If the JWT has an expiration (exp), it will be checked.

If you are using a base64 URL-encoded secret, pass a Buffer with base64 encoding as the secret instead of a string:

jwt({
  secret: Buffer.from("shhhhhhared-secret", "base64"),
  algorithms: ["RS256"],
});

To only protect specific paths (e.g. beginning with /api), use express router call use, like so:

app.use("/api", jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }));

Or, the other way around, if you want to make some paths unprotected, call unless like so.

app.use(
  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
  }).unless({ path: ["/token"] })
);

This is especially useful when applying to multiple routes. In the example above, path can be a string, a regexp, or an array of any of those.

For more details on the .unless syntax including additional options, please see express-unless.

This module also support tokens signed with public/private key pairs. Instead of a secret, you can specify a Buffer with the public key

var publicKey = fs.readFileSync("/path/to/public.pub");
jwt({ secret: publicKey, algorithms: ["RS256"] });

Customizing Token Location

A custom function for extracting the token from a request can be specified with the getToken option. This is useful if you need to pass the token through a query parameter or a cookie. You can throw an error in this function and it will be handled by express-jwt.

app.use(
  jwt({
    secret: "hello world !",
    algorithms: ["HS256"],
    credentialsRequired: false,
    getToken: function fromHeaderOrQuerystring(req) {
      if (
        req.headers.authorization &&
        req.headers.authorization.split(" ")[0] === "Bearer"
      ) {
        return req.headers.authorization.split(" ")[1];
      } else if (req.query && req.query.token) {
        return req.query.token;
      }
      return null;
    },
  })
);

Retrieve key dynamically

If you need to obtain the key dynamically from other sources, you can pass a function in the secret parameter with the following parameters:

  • req (Object) - The express request object.
  • token (Object) - An object with the JWT payload and headers.

For example, if the secret varies based on the issuer:

var jwt = require("express-jwt");
var data = require("./data");
var utilities = require("./utilities");

var getSecret = async function (req, token) {
  const issuer = token.payload.iss;
  const tenant = await data.getTenantByIdentifier(issuer);
  if (!tenant) {
    throw new Error("missing_secret");
  }
  return utilities.decrypt(tenant.secret);
};

app.get(
  "/protected",
  jwt({ secret: getSecret, algorithms: ["HS256"] }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Secret rotation

The getSecret callback could also be used in cases where the same issuer might issue tokens with different keys at certain point:

var getSecret = async function (req, token) {
  const { iss } = token.payload;
  const { kid } = token.header;
  // get the verification key by a given key-id and issuer.
  return verificationKey;
};

Revoked tokens

It is possible that some tokens will need to be revoked so they cannot be used any longer. You can provide a function as the isRevoked option. The signature of the function is function(req, payload, done):

  • req (Object) - The express request object.
  • token (Object) - An object with the JWT payload and headers.

For example, if the (iss, jti) claim pair is used to identify a JWT:

const jwt = require("express-jwt");
const data = require("./data");

const isRevokedCallback = async (req, token) => {
  const issuer = token.payload.iss;
  const tokenId = token.payload.jti;
  const token = await data.getRevokedToken(issuer, tokenId);
  return token !== "undefined";
};

app.get(
  "/protected",
  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
    isRevoked: isRevokedCallback,
  }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Handling expired tokens

You can handle expired tokens as follows:

  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
    onExpired: async (req, err) => {
      if (new Date() - err.inner.expiredAt < 5000) { return;}
      throw err;
    },,
  })

Error handling

The default behavior is to throw an error when the token is invalid, so you can add your custom logic to manage unauthorized access as follows:

app.use(function (err, req, res, next) {
  if (err.name === "UnauthorizedError") {
    res.status(401).send("invalid token...");
  } else {
    next(err);
  }
});

You might want to use this module to identify registered users while still providing access to unregistered users. You can do this by using the option credentialsRequired:

app.use(
  jwt({
    secret: "hello world !",
    algorithms: ["HS256"],
    credentialsRequired: false,
  })
);

Typescript

A Request type is provided from express-jwt, which extends express.Request with the auth property. It could be aliased, like how JWTRequest is below.

import { expressjwt, Request as JWTRequest } from "express-jwt";

app.get(
  "/protected",
  expressjwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }),
  function (req: JWTRequest, res: express.Response) {
    if (!req.auth?.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Migration from v6

  1. The middleware function is now available as a named import rather than a default one: import { expressjwt } from 'express-jwt'
  2. The decoded JWT payload is now available as req.auth rather than req.user
  3. The secret function had (req, header, payload, cb), now it can return a promise and receives (req, token). token has header and payload.
  4. The isRevoked function had (req, payload, cb), now it can return a promise and receives (req, token). token has header and payload.

Related Modules

Tests

$ npm install
$ npm test

Contributors

Check them out here

Issue Reporting

If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.

Author

Auth0

License

This project is licensed under the MIT license. See the LICENSE file for more info.