jsonwebtoken、koa-jwt、koa-passport、passport-jwt は、すべて JSON Web Token (JWT) を扱いますが、役割と抽象化のレベルが異なります。jsonwebtoken は JWT の署名と検証を行う低レベルなコアライブラリです。koa-jwt は Koa フレームワーク専用のミドルウェアで、jsonwebtoken の機能をラップしています。passport-jwt は Passport.js エコシステム向けの JWT 認証戦略であり、koa-passport は Passport を Koa で動作させるためのアダプターです。これらを組み合わせることで、シンプルな認証から複雑なマルチ戦略認証まで実現できます。
JWT(JSON Web Token)を用いた認証は、現代の Web アプリケーションにおいて標準的な仕組みとなっています。しかし、実装方法にはいくつかの選択肢があり、それぞれに適したシナリオが異なります。jsonwebtoken、koa-jwt、koa-passport、passport-jwt の 4 つのパッケージは、一見すると同じ目的のために存在するように見えますが、実際には異なるレイヤーで動作します。
本稿では、これらのパッケージの技術的な違い、実装コスト、そして拡張性の観点から比較し、プロジェクトに最適な選択をするための指針を提供します。
認証フローの根幹となるのは、トークンの「発行(署名)」と「検証」です。この基本機能を提供しているのは jsonwebtoken だけです。他の 3 つは、この機能をラップしてミドルウェアとして提供しています。
jsonwebtoken
最も低レベルなライブラリです。HTTP リクエストの処理は行わず、純粋なトークン操作に特化しています。
const jwt = require('jsonwebtoken');
// トークンの発行
const token = jwt.sign({ userId: 123 }, 'SECRET_KEY', { expiresIn: '1h' });
// トークンの検証
try {
const decoded = jwt.verify(token, 'SECRET_KEY');
console.log(decoded.userId); // 123
} catch (err) {
console.error('Invalid token');
}
koa-jwt
内部で jsonwebtoken を使用していますが、Koa のミドルウェアとして動作します。リクエストヘッダーからのトークン抽出を自動で行います。
const jwt = require('koa-jwt');
// ミドルウェアとして設定
app.use(jwt({ secret: 'SECRET_KEY' }).unless({ path: ['/public'] }));
// 検証済みのユーザー情報は ctx.state.user に格納される
app.use(async (ctx) => {
ctx.body = ctx.state.user;
});
passport-jwt
Passport.js の「戦略(Strategy)」として機能します。単体では Koa や Express のミドルウェアにはならず、Passport の初期化プロセス内で定義されます。
const JwtStrategy = require('passport-jwt').Strategy;
// 戦略の定義
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'SECRET_KEY'
},
async (jwt_payload, done) => {
// ユーザー検索ロジック
const user = await findUser(jwt_payload.id);
return done(null, user);
}
));
koa-passport
Passport を Koa で動作させるためのアダプターです。passport-jwt で定義した戦略を、Koa のミドルウェアとして登録するために使用します。
const passport = require('koa-passport');
// Passport の初期化
app.use(passport.initialize());
// ルートでの認証
router.get('/profile', passport.authenticate('jwt', { session: false }), async (ctx) => {
ctx.body = ctx.state.user;
});
フレームワークとの統合方法は、開発体験(DX)に大きな影響を与えます。koa-jwt は Koa 専用に設計されているため非常にシンプルですが、passport-jwt + koa-passport の組み合わせは設定が複雑になる代わりに柔軟性を提供します。
koa-jwt:Koa ネイティブな統合
Koa のコンテキスト(ctx)に直接依存しているため、設定が最小限で済みます。Authorization ヘッダーの解析もデフォルトで処理されます。
// koa-jwt の場合
// 1. ミドルウェアを登録するだけ
app.use(koaJwt({ secret: 'KEY' }));
// 2. ctx.state.user ですぐに使える
koa-passport + passport-jwt:アダプターを介した統合
Passport は本来 Express 向けに設計されています。koa-passport が Request/Response オブジェクトを変換して橋渡しを行います。このため、初期化ステップが一つ増えます。
// koa-passport の場合
// 1. 戦略を登録
passport.use('jwt', new JwtStrategy({ ... }, verifyCallback));
// 2. 初期化ミドルウェアを登録
app.use(passport.initialize());
// 3. ルートごとに authenticate を呼ぶ
router.get('/', passport.authenticate('jwt'), handler);
プロジェクトの規模が大きくなると、認証フローの細かい制御が必要になることがあります。ここでは、エラーハンドリングやトークン抽出のカスタマイズ性を比較します。
トークン抽出のカスタマイズ
デフォルトでは Authorization: Bearer <token> ヘッダーからトークンを取得しますが、クエリパラメータや Cookie から取得したい場合があります。
jsonwebtoken
完全に手動です。どこからトークンを取得するかは開発者が自由に実装します。
// 手動で Cookie から取得
const token = ctx.cookies.get('access_token');
const user = jwt.verify(token, 'SECRET');
koa-jwt
getToken オプションでカスタマイズ可能です。
app.use(koaJwt({
secret: 'SECRET',
getToken: (ctx) => ctx.cookies.get('access_token')
}));
passport-jwt
jwtFromRequest で抽出関数を指定します。 extractor の種類が豊富です。
new JwtStrategy({
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
(req) => req.cookies?.access_token // カスタム抽出
]),
secretOrKey: 'SECRET'
}, verifyCallback);
エラーハンドリング
認証失敗時の挙動も重要です。デフォルトでは 401 エラーを返しますが、独自のレスポンスを返したい場合があります。
koa-jwt
unless オプションで除外パスを設定できますが、エラーハンドリングはミドルウェアチェーンで捕捉します。
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
if (err.status === 401) {
ctx.status = 401;
ctx.body = { message: 'カスタムエラーメッセージ' };
}
}
});
koa-passport
authenticate オプションでコールバックを渡すことで、失敗時の処理をルーティングレベルで制御できます。
router.get('/protected', passport.authenticate('jwt', { session: false }, (err, user) => {
if (!user) {
ctx.status = 401;
ctx.body = { message: 'カスタムエラーメッセージ' };
return;
}
ctx.state.user = user;
}));
将来的に JWT 以外の認証(Google、GitHub、ローカル認証など)を追加する予定があるかどうかが、選定の決定的な要因になります。
jsonwebtoken / koa-jwt
JWT 専用です。他の認証方式を追加するには、別途ミドルウェアを実装するか、別のライブラリを導入する必要があります。エコシステムは単一目的に特化しています。
passport-jwt / koa-passport
Passport.js エコシステムの一部です。passport-local(ID/ パスワード)や passport-google-oauth などを同じ設定で追加できます。認証戦略が統一されるため、大規模なアプリケーションで管理コストが下がります。
// Passport エコシステムなら複数の戦略を併用可能
passport.use(new LocalStrategy(...));
passport.use(new GoogleStrategy(...));
passport.use(new JwtStrategy(...));
// すべて koa-passport で統一的に扱える
router.post('/login', passport.authenticate('local'));
router.get('/oauth', passport.authenticate('google'));
router.get('/api', passport.authenticate('jwt'));
| 機能 | jsonwebtoken | koa-jwt | passport-jwt + koa-passport |
|---|---|---|---|
| 抽象化レベル | 低(ユーティリティ) | 中(ミドルウェア) | 高(フレームワーク + 戦略) |
| フレームワーク | 非依存 | Koa 専用 | Koa (Passport アダプター経由) |
| 設定の手間 | 手動実装が必要 | 最小限 | 初期化と戦略定義が必要 |
| 拡張性 | 低い(JWT のみ) | 低い(JWT のみ) | 高い(多様な戦略と併用可能) |
| エラー制御 | 完全手動 | ミドルウェアチェーン | コールバックで細かく制御 |
| 推奨ユースケース | カスタム実装、学習用 | シンプルな Koa API | 複雑な認証要件、複数戦略 |
プロジェクトの要件に応じて、以下のように選択するのが賢明です。
1. シンプルな Koa API サーバーの場合
koa-jwt を選択してください。設定が簡単で、Koa の哲学に合致しています。JWT 認証だけで完結するプロジェクトでは、Over-Engineering(過剰設計)を避けることができます。
2. 複数の認証方式を扱う場合
passport-jwt と koa-passport の組み合わせを選択してください。将来的に OAuth や ID/ パスワード認証を追加する可能性があるなら、最初から Passport エコシステムに載せておくことで、後々のリファクタリングコストを削減できます。
3. 完全な制御が必要な場合
jsonwebtoken を直接使用してください。ミドルウェアの挙動に不満がある場合や、独自の認証プロトコルを構築する場合には、このライブラリが最も柔軟です。ただし、セキュリティ実装の責任はすべて開発者にあります。
結論として、多くの現代的な Koa プロジェクトでは、koa-jwt がバランスの取れた選択です。しかし、エンタープライズレベルの認証要件がある場合は、Passport エコシステムへの投資が長期的なメンテナンス性を高めます — どちらを選ぶにせよ、jsonwebtoken がその根幹にあることを理解しておくことが重要です。
Koa を使用しつつ、Passport の豊富な戦略(passport-jwt など)を利用したい場合に必須です。Passport の初期化とルーティングを Koa 向けに変換する役割を果たします。
カスタムミドルウェアを作成したい場合や、フレームワークに依存しない純粋な JWT 操作が必要な場合は jsonwebtoken を選択します。認証フローの每一个细节を制御したい上級者向けです。
Koa を使用していて、JWT 認証のみが必要で、追加の認証戦略(OAuth など)を予定していない場合は koa-jwt が最適です。設定がシンプルで、Koa のコンテキストに自然に統合されます。
Passport.js のエコシステムを利用する場合、特に JWT 以外の認証戦略(Google ログインなど)と組み合わせたい場合は passport-jwt を選択します。単体ではミドルウェアとして動作しないため、アダプターと併用が必要です。
Passport middleware for Koa
| koa-passport version | passport version | koa version | branch |
|---|---|---|---|
| 6.x, 5.x | 6.x, 5.x | 2.x | main |
| 4.x | 4.x | 2.x | v3.x |
| 3.x, 2.x | 2.x | 2.x | v2.x |
| 1.x | 1.x | 1.x | v1.x |
// body parser
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
// Sessions
const session = require('koa-session')
app.keys = ['secret']
app.use(session({}, app))
const passport = require('koa-passport')
app.use(passport.initialize())
app.use(passport.session())
Passport's values and methods are exposed as follows:
app.use(async ctx => {
ctx.isAuthenticated()
ctx.isUnauthenticated()
await ctx.login()
ctx.logout()
ctx.state.user
})