Menu
CoddyTech

What a JWT Is

Part of the Auth and Security section of Coddy's Node.js journey. Lesson 9 of 17.

A session needs a database lookup on every request. A JWT (JSON Web Token) carries the facts itself: three base64url parts joined by dots. The header names the algorithm, the payload holds the claims (sub for whose token it is, exp for when it expires), and the signature proves the server made it.

Base64url is an encoding, not encryption. Anyone holding a token can read its payload:

const payload = token.split('.')[1];
JSON.parse(Buffer.from(payload, 'base64url').toString());
// { sub: '42', name: 'Lin', exp: 1767225600 }

decodeJwt(token) from jose does the same and skips the signature. So never put a secret in a payload, and never decide anything from a claim decodeJwt gave you: anyone can write any claim.

challenge icon

Challenge

Easy
  • GET /inspect?token=... answers the token's claims as JSON, or 400 { "error": "Not a JWT" }.
  • GET /admin answers 403 { "error": "Admins only" } to every request.

First send Mallory's token to GET /admin?token= and see what it opens: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl. Fix it, then send the same token to /inspect.

Try it yourself

import express from 'express';
import { decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

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 });
});

// Shows what a token says: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    res.status(501).json({ error: 'Not written yet' });
});

// The admin page.
app.get('/admin', (req, res) => {
    const claims = decodeJwt(String(req.query.token ?? ''));
    if (claims.admin === true) {
        return res.json({ secret: 'The vault key is 42' });
    }
    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