Menu
CoddyTech

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 icon

Challenge

Easy

Now 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.

  1. In server.js, import jwtVerify from jose.
  2. Write a middleware requireUser(req, res, next). It reads the Authorization header. That header holds the word Bearer, a space, then the token. Take the token out of it.
  3. Check the token with jwtVerify, using the same key you sign with. jwtVerify throws 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"}.
  4. When the token is valid, set req.user to an object with id, taken from the token's sub claim, and username, taken from its username claim. Then call next().
  5. Add GET /me with requireUser. It returns req.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');
});
quiz iconTest yourself

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