jsonwebtoken は JWT の生成・検証を行う低レベルなコアライブラリであり、express-jwt や koa-jwt はこれを Express や Koa フレームワーク用にラップしたミドルウェアです。passport-jwt は Passport.js エコシステム内で戦略として動作し、express-jwt-authz は認可(スコープチェック)に特化しています。一方、jwt-simple は軽量ですが現在では非推奨とされており、セキュリティ上の理由から新規プロジェクトでの使用は避けるべきです。これらを適切に使い分けることで、安全かつ保守性の高い認証システムを構築できます。
Node.js で JWT(JSON Web Token)を使った認証システムを構築する際、多くの開発者は「どのパッケージを使うべきか」で迷います。jsonwebtoken、express-jwt、passport-jwt など、似たような名前や機能を持つパッケージが乱立しているためです。
本記事では、これら主要な 6 つのパッケージを技術的な観点から深く比較します。単なる機能リストではなく、実際のコード例を通じて「いつ、どれを選ぶべきか」を明確にします。
まず大前提として、これらのパッケージは 2 つのカテゴリに分類できます。
jsonwebtoken: すべての中核となるコアライブラリjsonwebtoken は、Auth0 によってメンテナンスされている事実上の標準ライブラリです。フレームワークに依存せず、JWT の発行(sign)と検証(verify)のみを行います。
const jwt = require('jsonwebtoken');
const secret = 'my-secret-key';
// トークンの発行
const token = jwt.sign({ userId: 123, role: 'admin' }, secret, { expiresIn: '1h' });
// トークンの検証(ミドルウェア内で手動呼び出しが必要)
try {
const decoded = jwt.verify(token, secret);
console.log(decoded); // { userId: 123, role: 'admin', iat: ... }
} catch (err) {
console.error('Invalid token');
}
選定理由: Express や Koa に限定されない汎用性が必要な場合、あるいはミドルウェアの挙動を完全にカスタマイズしたい場合に必須です。他の多くのパッケージも内部でこれを利用しています。
jwt-simple: ⚠️ 非推奨のパッケージjwt-simple は、昔から存在する軽量な JWT ライブラリですが、現在は機能が限定的で、セキュリティアップデートも頻繁ではありません。npm のページやリポジトリでも、より堅牢な jsonwebtoken への移行が事実上推奨されています。
// jwt-simple の例(非推奨)
const jwt = require('jwt-simple');
const token = jwt.encode({ userId: 123 }, 'secret');
const decoded = jwt.decode(token, 'secret');
選定理由: 新規プロジェクトでは使用しないでください。サポートが不十分で、アルゴリズムの柔軟性も低いため、セキュリティリスクになります。必ず jsonwebtoken を選んでください。
Express を使っている場合、jsonwebtoken を毎回手動で呼び出すのは面倒です。そこでミドルウェアの出番です。
express-jwt: 標準的な自動検証ミドルウェアexpress-jwt は、リクエストヘッダーからトークンを抽出し、検証して req.user に結果を格納するまでの処理を自動化します。
const express = require('express');
const { expressjwt: jwt } = require('express-jwt');
const app = express();
// ミドルウェアとして適用
app.use(
jwt({
secret: 'my-secret-key',
algorithms: ['HS256'],
getToken: (req) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
return null;
}
})
);
// 保護されたルート
app.get('/profile', (req, res) => {
// req.user には検証済みのデコードデータが入っている
res.json({ user: req.user });
});
選定理由: Express で標準的な API 認証を実装する場合、最もシンプルで確実な選択です。ボイラープレートコードを削減できます。
passport-jwt: 複雑な認証戦略の一部としてpassport-jwt は、Passport.js エコシステムの一部です。単独で使うというより、ローカル認証(ID/Pass)や Google 認証などと併用し、戦略を切り替えたい場合に威力を発揮します。
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'my-secret-key'
};
// 戦略の定義
passport.use(new JwtStrategy(options, (jwt_payload, done) => {
// ここで DB 検索など追加ロジックを挟める
User.findById(jwt_payload.userId, (err, user) => {
if (err) return done(err, false);
return done(null, user || false);
});
}));
// ルートでの使用
app.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
res.json({ user: req.user });
});
選定理由: 単なるトークン検証だけでなく、「トークン内の ID で DB からユーザー情報を再取得する」といった処理や、複数の認証方法を組み合わせたい場合に適しています。設定は express-jwt よりも重くなります。
express-jwt-authz: 認可(Authorization)に特化express-jwt-authz は、認証(Authentication)ではなく認可(Authorization)を担当します。express-jwt で検証が終わった後、「このユーザーは admin スコープを持っているか?」をチェックするために使います。
const { authz } = require('express-jwt-authz');
// express-jwt の後に適用
app.get('/admin-dashboard',
authz(['read:users', 'write:users']), // これらのスコープが必要
(req, res) => {
res.json({ message: 'Welcome Admin' });
}
);
選定理由: スコープベースのアクセス制御が必要な場合、express-jwt とセットで使用します。単体では動作しません。
Koa ユーザーが Express 用ミドルウェアを使うと、非同期処理の扱い(Promise vs Callback)で問題が起きます。Koa 専用のパッケージを使いましょう。
koa-jwt: Koa 向けの最適化ミドルウェアkoa-jwt は、express-jwt と同じ機能を提供しますが、Koa の ctx オブジェクトや async/await 構文に最適化されています。
const Koa = require('koa');
const koaJwt = require('koa-jwt');
const app = new Koa();
// Koa ミドルウェアとして適用
app.use(
koaJwt({
secret: 'my-secret-key',
algorithms: ['HS256']
}).unless({ path: ['/public', '/login'] })
);
app.use(async (ctx) => {
// ctx.state.user にデコードデータが入る
ctx.body = { user: ctx.state.user };
});
選定理由: Koa を使っている場合、これ一択です。Express 用パッケージをアダプター経由で使うより、パフォーマンスと保守性の面で優れています。
| パッケージ | 主な役割 | 対応フレームワーク | 自動ユーザー注入 | 認可機能 | 推奨度 |
|---|---|---|---|---|---|
jsonwebtoken | コア処理 (Sign/Verify) | 汎用 (None) | ❌ (手動) | ❌ | ⭐⭐⭐⭐⭐ (必須) |
express-jwt | 認証ミドルウェア | Express | ✅ (req.user) | ❌ | ⭐⭐⭐⭐⭐ |
koa-jwt | 認証ミドルウェア | Koa | ✅ (ctx.state.user) | ❌ | ⭐⭐⭐⭐⭐ (Koa 限定) |
passport-jwt | 認証戦略 | Express (Passport) | ✅ (req.user) | ❌ | ⭐⭐⭐⭐ (複雑な場合) |
express-jwt-authz | 認可ミドルウェア | Express | ❌ (前提必要) | ✅ | ⭐⭐⭐⭐ (併用時) |
jwt-simple | コア処理 (旧) | 汎用 | ❌ | ❌ | ❌ (非推奨) |
最も一般的な構成です。余計なものは入れず、express-jwt で守ります。
express + express-jwtユーザーの役割(Role)やスコープを細かく制御する場合です。
express + express-jwt + express-jwt-authzログイン(ID/Pass)、ソーシャルログイン、API トークン認証を全て支える場合です。
express + passport + passport-jwt (+ passport-local etc)koa + koa-jwtどのパッケージを選ぶにしても、以下の点は共通して重要です。
jsonwebtoken や express-jwt では、algorithms: ['HS256'] のように使用するアルゴリズムを明示的に指定してください。指定しないと、アルゴリズム_none_ 攻撃などのリスクが高まります。expiresIn を設定し、トークンの寿命を短くすることで、漏洩時のリスクを軽減します。express-jwt がベストプラクティスです。koa-jwt 一択です。jsonwebtoken を直接使うか、passport-jwt を検討します。jwt-simple はレガシーなプロジェクトの維持以外は選択肢から外してください。これらのツールは「車輪の再発明」を防ぎ、セキュリティの落とし穴を埋めるために存在します。プロジェクトの規模と複雑度に合わせて、適切なツールを選ぶことが、堅牢なシステム構築への第一歩です。
Express アプリケーションで、リクエストごとに JWT を自動検証し、ユーザー情報を req.user に注入したい場合に選択します。設定がシンプルで、標準的な API サーバーの認証ゲートウェイとして最適です。ただし、複雑な認証フローや複数戦略の併用には向きません。
JWT の検証自体ではなく、検証済みのトークンに含まれる権限(スコープやパーミッション)に基づいてアクセス制御を行いたい場合に使用します。通常は express-jwt と組み合わせて、認証の次のステップとして導入します。
フレームワークに依存せず、JWT の署名、検証、デコードを自前で制御したい場合に選択します。ミドルウェアとしての自動処理は行わないため、Koa や Fastify など Express 以外の環境や、カスタムな認証ロジックが必要な場合に適しています。
非常に軽量な実装ですが、機能制限があり、現在はメンテナンスが停滞しているため新規プロジェクトでの使用は推奨されません。セキュリティリスクを避けるため、代わりに jsonwebtoken の使用を検討すべきです。
Koa フレームワークを使用している場合に選択する、express-jwt と同等のミドルウェアです。Koa のミドルウェア構成(async/await ベース)に最適化されており、Express 用パッケージを Koa で無理に使うべきではありません。
Passport.js を既に採用しており、ローカル認証、OAuth、JWT など複数の認証戦略を切り替え可能にしたい場合に選択します。単なる 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.
$ npm install express-jwt
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.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;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
Authorizationheader as an OAuth2 Bearer token.
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']
});
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
.unlesssyntax 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"] });
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;
},
})
);
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);
}
);
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;
};
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);
}
);
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;
},,
})
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,
})
);
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);
}
);
secret function had (req, header, payload, cb), now it can return a promise and receives (req, token). token has header and payload.isRevoked function had (req, payload, cb), now it can return a promise and receives (req, token). token has header and payload.$ npm install
$ npm test
Check them out here
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.
This project is licensed under the MIT license. See the LICENSE file for more info.