Issuing and Verifying Tokens with koa-jwt
JWT Authentication
koa-jwt verifies Bearer tokens on protected routes and exposes the decoded payload on ctx.state.user. Pair it with jsonwebtoken to issue tokens on login.
What you'll learn
- Issue signed JWTs on login using jsonwebtoken
- Protect routes with koa-jwt middleware
- Access ctx.state.user and selectively bypass protection with passthrough/unless
JWTs (JSON Web Tokens) are signed, not encrypted. Anyone who holds the
token can decode its payload — never put passwords, full PII, or secrets
inside. Sign with a long, random secret (JWT_SECRET) stored in an environment
variable.
Installation
npm install koa-jwt jsonwebtoken Issuing a Token on Login
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import { findUserByEmail } from "./db.js";
router.post("/auth/login", async (ctx) => {
const { email, password } = ctx.request.body;
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
ctx.status = 401;
ctx.body = { error: "Invalid credentials" };
return;
}
// Payload: minimal, non-sensitive claims only.
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "2h", algorithm: "HS256" }
);
ctx.body = { token };
}); Protecting Routes with koa-jwt
Mount koa-jwt before any route that needs protection. It reads the
Authorization: Bearer <token> header, verifies the signature, and places
the decoded payload on ctx.state.user.
import Koa from "koa";
import koaJwt from "koa-jwt";
const app = new Koa();
// Protect everything below this middleware.
app.use(
koaJwt({ secret: process.env.JWT_SECRET, algorithms: ["HS256"] })
);
// ctx.state.user is available here.
app.use(async (ctx) => {
ctx.body = { userId: ctx.state.user.sub };
}); Bypassing Protection for Public Routes
Use unless to let public endpoints through without a token:
app.use(
koaJwt({ secret: process.env.JWT_SECRET, algorithms: ["HS256"] }).unless({
path: [/^\/auth/, /^\/public/],
})
); Or use passthrough: true to continue even without a token (the middleware
will not set ctx.state.user), letting your route decide:
app.use(
koaJwt({
secret: process.env.JWT_SECRET,
algorithms: ["HS256"],
passthrough: true,
})
);
app.use(async (ctx) => {
if (!ctx.state.user) {
// anonymous request
}
}); Token Expiry and Revocation
JWTs are valid until exp. To invalidate a token early you need a server-side
blocklist (e.g., a Redis set of revoked jti claims). Check the blocklist in a
custom middleware immediately after koa-jwt:
app.use(async (ctx, next) => {
const { jti } = ctx.state.user ?? {};
if (jti && (await redis.sIsMember("revoked_tokens", jti))) {
ctx.status = 401;
ctx.body = { error: "Token revoked" };
return;
}
await next();
}); Up Next
Add strategy-based auth (local, social) to your Koa app with koa-passport.