Verifying Tokens
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 16 of 24.
An API client sends its token in a header on every request: Authorization: Bearer <token>. Reading it with decodeJwt would accept any token anyone typed. jwtVerify(token, key) recomputes the signature with your secret and throws when it does not match, when the token expired, or when it is not a JWT at all. Only after it returns may you trust payload.
async function requireDevice(req, res, next) {
const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
try {
const { payload } = await jwtVerify(token, deviceKey);
req.device = payload.sub;
} catch {
return res.status(401).json({ error: 'Unknown device' });
}
next();
}Challenge
EasyNow accept the tokens you sign. An app sends its token in a header on every request, and the server checks the signature before it trusts anything inside.
- In
server.js, importjwtVerifyfromjose. - Write a middleware
requireUser(req, res, next). It reads theAuthorizationheader. That header holds the wordBearer, a space, then the token. Take the token out of it. - Check the token with
jwtVerify, using the same key you sign with.jwtVerifythrows when the signature does not match, when the token has expired, or when the text is not a token at all. In all those cases, and when there is no header, respond with status 401 and{"error": "Log in first"}. - When the token is valid, set
req.userto an object withid, taken from the token'ssubclaim, andusername, taken from itsusernameclaim. Then callnext(). - Add
GET /mewithrequireUser. It returnsreq.user:{"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.
Try it: Press Run. Send GET /me: status 401 and {"error": "Log in first"}. Send POST /signup, then POST /token, both with {"username": "ada", "password": "correct horse"}, and copy the token. In the Headers field, add a header named Authorization whose value is Bearer, a space, and your token. Send GET /me: status 200, with your id and "username": "ada". Now change one letter in the middle part of the token, between the two dots, and send GET /me again: status 401, because the signature no longer matches.
Try it yourself
import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
res.json({ username: user.username });
});
app.post('/token', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
const token = await new SignJWT({ username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(user._id.toHexString())
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
res.json({ token });
});
// Shows what a token says: GET /inspect?token=...
app.get('/inspect', (req, res) => {
try {
res.json(decodeJwt(String(req.query.token ?? '')));
} catch {
res.status(400).json({ error: 'Not a JWT' });
}
});
// The admin page. No claim can be trusted before its signature is checked.
app.get('/admin', (req, res) => {
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Auth and Security
Practice on your own: Express playground