Menu
CoddyTech

Was ein JWT ist

Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 14 von 24.

Eine Sitzung benötigt bei jeder Anfrage eine Datenbanksuche. Ein JWT (JSON Web Token) trägt die Informationen selbst: drei durch Punkte verbundene Teile. Jeder Teil ist in base64url geschrieben, einer Methode, Bytes mit Buchstaben, Ziffern, - und _ darzustellen. Der Header benennt den Algorithmus, der Payload enthält die Ansprüche (sub gibt an, wem das Token gehört, exp, wann es abläuft), und die Signatur beweist, dass der Server es erstellt hat.

Base64url ist eine Kodierung, keine Verschlüsselung. Jede Person, die ein Token besitzt, kann dessen Payload lesen:

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

decodeJwt(token) aus jose, dem JWT-Paket, macht dasselbe. Es liest den Payload, ohne die Signatur zu überprüfen, und löst einen Fehler aus, wenn der Text kein JWT ist. Lege also niemals ein Geheimnis in einen Payload und entscheide niemals etwas anhand eines Anspruchs, den dir decodeJwt geliefert hat: Jede Person kann beliebige Ansprüche schreiben.

challenge icon

Aufgabe

Einfach

Dieses Kapitel ergänzt eine zweite Möglichkeit zur Anmeldung mit Tokens für Apps, die deine API aufrufen. Es beginnt mit einer kleineren Kopie des Servers, mit Registrierung und Anmeldung, aber ohne Sitzungen oder Notizen. Ein JWT enthält seine Angaben in sich selbst, und jede Person kann sie lesen. Erstelle eine Route, die zeigt, was ein Token aussagt, und sperre eine Admin-Seite, die allem vertraut, was ein Token behauptet.

  1. Schreibe in server.js GET /inspect. Die Route liest ein Token aus der Abfragezeichenfolge, wie in GET /inspect?token=eyJ..., und gibt die darin enthaltenen Angaben als JSON zurück. Lies sie mit decodeJwt aus, das bereits importiert ist. Bei einem Token, dessen Payload {"sub": "lin", "plan": "free"} ist, gibt die Route genau {"sub": "lin", "plan": "free"} zurück.
  2. decodeJwt löst einen Fehler aus, wenn der Text kein JWT ist. Fange ihn ab und antworte mit Status 400 und {"error": "Not a JWT"}. Zum Beispiel erhält GET /inspect?token=hello diesen Status 400.
  3. Sieh dir GET /admin an. Die Route liest das Token mit decodeJwt aus und zeigt ein Geheimnis an, wenn das Token "admin": true aussagt. Aber decodeJwt prüft niemals die Signatur, daher kann jede Person ein Token eingeben, das "admin": true aussagt. Dieser Server kann Signaturen noch nicht prüfen. Lass deshalb GET /admin bei jeder Anfrage mit Status 403 und {"error": "Admins only"} antworten, unabhängig davon, welches Token er erhält. 403 Forbidden bedeutet „Du darfst das nicht sehen“. Kapitel 4 öffnet wieder eine Admin-Seite, diesmal wirklich nur für Admins.

Probiere es aus: Drücke auf „Run“. In der Anfrageleiste steht zunächst GET /admin?token=, gefolgt von diesem Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl Ein Angreifer namens mallory hat es von Hand und ohne geheimen Schlüssel geschrieben. Sende es, bevor du etwas änderst: Der Starter gibt {"secret": "The vault key is 42"} zurück. Führe nach deiner Änderung „Run“ erneut aus und sende dieselbe Anfrage: Status 403 und {"error": "Admins only"}. Sende GET /inspect?token=, gefolgt von demselben Token: {"sub": "mallory", "admin": true}, ausgelesen ganz ohne Schlüssel. Sende GET /inspect?token=hello: Status 400 und {"error": "Not a JWT"}.

Probier es selbst

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

// Zeigt, was ein Token enthält: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    res.status(501).json({ error: 'Not written yet' });
});

// Die Admin-Seite.
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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Authentifizierung und Sicherheit

Übe selbstständig: Express-Playground