Koa JWT Middleware
koa-jwt is a middleware for Koa.js applications designed to authenticate HTTP requests using JSON Web Tokens (JWTs). It parses and validates JWTs typically provided in the `Authorization` header, or optionally from a cookie or a custom `getToken` function. Upon successful validation, the decoded JWT payload is exposed on `ctx.state.user` (by default) for subsequent middleware to use for authorization and access control. The current stable version is 4.0.4. Releases are driven by dependency updates (especially `jsonwebtoken`) and bug fixes, with major versions tied to Node.js support or significant internal changes. It differentiates itself by providing a streamlined, Koa-idiomatic approach to JWT authentication, leveraging Koa's async/await middleware pattern, and integrates well with `koa-unless` for path-based exclusion. It supports single or multiple secrets, including rolling secrets or mixed authentication methods (e.g., Auth0 PEM files and shared secrets).
Common errors
-
`koa-jwt` failed to verify token: secret or public key must be provided
cause The `secret` option was not provided to the `koa-jwt` middleware, or `ctx.state.secret` was not set, and a token requiring verification was present.fixProvide a `secret` string or buffer in the `koa-jwt` middleware options, e.g., `app.use(jwt({ secret: 'your-secret' }))`. Alternatively, if using dynamic secrets, ensure a preceding middleware correctly sets `ctx.state.secret`. -
TokenExpiredError: jwt expired
cause The JSON Web Token presented in the request has expired according to its `exp` claim.fixThe client needs to obtain a new, valid (unexpired) JWT from your authentication endpoint. On the server side, you can catch `TokenExpiredError` specifically in your error handling middleware to return a more informative response. -
JsonWebTokenError: invalid signature
cause The signature of the JWT does not match the computed signature, indicating the token has been tampered with or signed with a different secret than the one `koa-jwt` is using for verification.fixEnsure the `secret` used by `koa-jwt` on the server matches *exactly* the secret used to sign the token. Check for environmental variable mismatches, trimming issues, or different keys for different services. -
`TypeError: jwt is not a function`
cause Incorrect import statement for `koa-jwt` in an ESM context, or attempting to `require` an ESM-only module in a CommonJS context, or incorrect destructuring of the default export.fixFor ESM, ensure you are using `import jwt from 'koa-jwt';`. For CommonJS, `const jwt = require('koa-jwt');` is correct. Avoid `import { jwt } from 'koa-jwt';` as it is a default export.
Warnings
- breaking Version 4.0.0 and above of `koa-jwt` require Node.js >= 8 due to the adoption of `async`/`await`. Prior versions (e.g., v3.x) required Node.js >= 7.6, and older versions (v2.x) supported Node.js < 7.6. Running on an unsupported Node.js version will lead to syntax errors or unexpected behavior.
- breaking Version 4.0.4 updated its underlying `jsonwebtoken` dependency from v8.5.1 to v9.0.0. This major update in `jsonwebtoken` introduces breaking changes, notably affecting the `jwt.verify` callback signature (error is now the first argument) and `jwt.decode` no longer throwing errors for invalid tokens (it returns `null` instead). While `koa-jwt` attempts to abstract this, custom `getToken` or `isRevoked` functions that directly interact with `jsonwebtoken`'s `verify` or `decode` might need adjustments. Additionally, `jsonwebtoken` v9 dropped Node.js v10 support, impacting minimum compatible Node.js versions for downstream projects.
- gotcha When the `debug` option is set to `true` (or implicitly `false` in older versions), `koa-jwt` might expose more detailed error messages on authentication failures, including potentially sensitive information about the token or verification process. This can be a security risk in production environments by aiding attackers in probing for vulnerabilities. Since v3.2.0, when `debug` is `false`, all thrown errors have the same generic message for security reasons.
- gotcha The middleware resolves tokens in a specific order: `opts.getToken` function, then `opts.cookie`, then the `Authorization` header. If `opts.getToken` is provided, it takes precedence. Overlooking this order can lead to unexpected token validation issues, where a token is picked from an undesired source.
- gotcha If `ctx.state.secret` is set by an earlier middleware, `koa-jwt` will use it instead of the `secret` provided in its options. This can be powerful for per-request secrets but can also lead to misconfigurations if an unintended secret is set on the context state, bypassing the middleware's configured secret.
Install
-
npm install koa-jwt -
yarn add koa-jwt -
pnpm add koa-jwt
Imports
- jwt
const { jwt } = require('koa-jwt');import jwt from 'koa-jwt';
- Options
import type { Options } from 'koa-jwt'; - Koa.Context augmentation
import type { Context } from 'koa'; // ... and koa-jwt adds ctx.state.user
Quickstart
import Koa from 'koa';
import jwt from 'koa-jwt';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import { sign } from 'jsonwebtoken';
const app = new Koa();
const router = new Router();
// A secret key for signing and verifying tokens. In a real app, use environment variables.
const SUPER_SECRET_KEY = process.env.JWT_SECRET ?? 'your-super-secret-jwt-key';
app.use(bodyParser());
// Unprotected route for login
router.post('/login', async (ctx) => {
const { username, password } = ctx.request.body as { username?: string, password?: string };
if (username === 'test' && password === 'password') {
// In a real app, you'd fetch user from DB and sign a token with user-specific data.
const token = sign({ id: 1, username: 'test' }, SUPER_SECRET_KEY, { expiresIn: '1h' });
ctx.body = { token };
} else {
ctx.status = 401;
ctx.body = { message: 'Invalid credentials' };
}
});
// JWT middleware protects all routes after this point, except those explicitly excluded.
// For this example, we'll manually exclude /login and /public using .unless() or a custom conditional middleware.
// A more robust solution often involves the 'koa-unless' package.
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/public') || ctx.path.startsWith('/login')) {
await next();
} else {
return jwt({ secret: SUPER_SECRET_KEY, debug: true })(ctx, next);
}
});
// Error handling for JWT authentication failures
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
if (401 === err.status) {
ctx.status = 401;
ctx.body = {
error: 'Protected resource, use Authorization header to get access',
details: err.originalError ? err.originalError.message : err.message
};
} else {
throw err;
}
}
});
// Protected route
router.get('/protected', async (ctx) => {
// ctx.state.user will contain the decoded JWT payload if authentication succeeded
ctx.body = `Hello, ${(ctx.state as any).user.username}! This is a protected route.`;
});
// Public route
router.get('/public', async (ctx) => {
ctx.body = 'This is a public route, no token needed.';
});
app.use(router.routes());
app.use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log('Try POST /login with {username: "test", password: "password"} in body');
console.log('Then GET /protected with "Authorization: Bearer <token>" header');
console.log('Or GET /public without any token');
});