Menu
CoddyTech

Cos'è un JWT

Fa parte della sezione Autenticazione e sicurezza del percorso Node.js di Coddy. Lezione 14 di 24.

Una sessione richiede una ricerca nel database a ogni richiesta. Un JWT (JSON Web Token) contiene i dati al suo interno: tre parti unite da punti. Ogni parte è scritta in base64url, un modo per rappresentare i byte usando lettere, cifre, - e _. L’intestazione indica l’algoritmo, il payload contiene le dichiarazioni (sub indica di chi è il token, exp indica quando scade) e la firma dimostra che è stato creato dal server.

Base64url è una codifica, non una crittografia. Chiunque abbia un token può leggere il suo payload:

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

decodeJwt(token) di jose, il pacchetto JWT, fa lo stesso. Legge il payload senza verificare la firma e genera un errore quando il testo non è un JWT. Quindi non inserire mai un segreto in un payload e non prendere mai decisioni basandoti su una dichiarazione fornita da decodeJwt: chiunque può scrivere qualsiasi dichiarazione.

challenge icon

Sfida

Facile

Questo capitolo aggiunge un secondo modo di accedere, con token, per le app che chiamano la tua API. Parte da una copia più piccola del server, con registrazione e accesso ma senza sessioni o note. Un JWT contiene al suo interno le proprie informazioni e chiunque può leggerle. Crea una route che mostri cosa dice un token e chiudi una pagina di amministrazione che si fida di tutto ciò che dichiara un token.

  1. In server.js, scrivi GET /inspect. Legge un token dalla stringa di query, come in GET /inspect?token=eyJ..., e restituisce le dichiarazioni che contiene in formato JSON. Leggile con decodeJwt, che è già stato importato. Per un token il cui payload è {"sub": "lin", "plan": "free"}, la route restituisce esattamente {"sub": "lin", "plan": "free"}.
  2. decodeJwt genera un errore quando il testo non è un JWT. Intercettalo e rispondi con lo stato 400 e {"error": "Not a JWT"}. Per esempio, GET /inspect?token=hello riceve quel 400.
  3. Esamina GET /admin. Legge il token con decodeJwt e mostra un segreto quando il token dichiara "admin": true. Ma decodeJwt non verifica mai la firma, quindi chiunque può scrivere un token che dichiara "admin": true. Questo server non è ancora in grado di verificare le firme. Quindi fai in modo che GET /admin risponda con lo stato 403 e {"error": "Admins only"} a ogni richiesta, qualunque token riceva. 403 Forbidden significa «non puoi vedere questo». Il capitolo 4 riapre una pagina di amministrazione, ma solo per i veri amministratori.

Prova: premi Run. La barra delle richieste parte da GET /admin?token= seguito da questo token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl Un aggressore chiamato mallory lo ha scritto a mano, senza una chiave segreta. Invialo prima di modificare qualsiasi cosa: la versione iniziale restituisce {"secret": "The vault key is 42"}. Dopo la modifica, premi di nuovo Run e invia la stessa richiesta: stato 403 e {"error": "Admins only"}. Invia GET /inspect?token= seguito dallo stesso token: {"sub": "mallory", "admin": true}, letto senza alcuna chiave. Invia GET /inspect?token=hello: stato 400 e {"error": "Not a JWT"}.

Provalo tu

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

// Mostra cosa contiene un token: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    res.status(501).json({ error: 'Not written yet' });
});

// La pagina di amministrazione.
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 iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Autenticazione e sicurezza

Esercitati da solo: Playground Express