Verifying Tokens
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 11 of 17.
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
Easy- Write
requireUser: verify theAuthorization: Bearertoken withjwtVerifyand setreq.user = { id, username }from its claims, or answer 401{ "error": "Log in first" }. GET /meuses it and answersreq.user.
Send GET /me without a token. Get one from POST /token, add the header Authorization: Bearer <token> and send GET /me again. Then change one letter of the token and send it once more.
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
2Sessions and Cookies
Remembering with CookiesLogin Sessions