Menu
CoddyTech

Tokens signieren

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

Ein Token ist nur dann etwas wert, wenn niemand sonst einen erstellen kann. Bei HS256 kombiniert jose den Header und die Nutzlast mit einem Geheimnis, das nur der Server kennt, und hasht das Ergebnis. Dieser Hash ist die Signatur. Ohne das Geheimnis kann niemand einen passenden Token erstellen.

Das Geheimnis stammt aus einer Umgebungsvariable, process.env.JWT_SECRET, niemals aus dem Code: Code wird geteilt, kopiert und eingecheckt. Diese Lektion legt JWT_SECRET für dich fest. jose benötigt es als Bytefolge.

const key = new TextEncoder().encode(process.env.INVITE_SECRET);

const invite = await new SignJWT({ room: 'design' })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(guestId)
    .setIssuedAt()
    .setExpirationTime('2d')
    .sign(key);

setSubject erwartet Text, und setExpirationTime erwartet eine Zeitspanne wie '30m' oder '2d'.

challenge icon

Aufgabe

Einfach

Apps, die deine API aufrufen, etwa eine Telefon-App, speichern Cookies nicht wie ein Browser. Gib ihnen stattdessen ein Token: ein signiertes JWT, das sie mit jeder Anfrage mitsenden. POST /token ist der Login für diese Apps.

  1. Importiere in server.js SignJWT aus jose neben decodeJwt. Wandle oberhalb der Routen process.env.JWT_SECRET mit einem TextEncoder in Bytes um, wie in der Lektion gezeigt, denn jose signiert mit Bytes. Diese Lektion setzt JWT_SECRET für dich.
  2. Füge POST /token hinzu. Es empfängt denselben Body wie der Login, {"username": "ada", "password": "correct horse"}, und prüft Benutzername und Passwort genauso wie POST /login. Wenn sie falsch sind, antworte mit Status 401 und {"error": "Wrong username or password"}.
  3. Wenn sie richtig sind, signiere ein Token und antworte damit als {"token": "..."}. Das Token muss:
    • den Algorithmus HS256 verwenden und mit den Bytes von JWT_SECRET signiert sein;
    • den Anspruch username in seiner Payload enthalten;
    • die _id des Benutzers als Text als Betreff haben. Verwende toHexString(), da jose einen Betreff ablehnt, der kein Text ist;
    • eine Stunde nach seiner Erstellung ablaufen.

    Ein Token aus dieser Lektion sieht so aus: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

Probiere es aus: Drücke „Ausführen“. Sende POST /signup mit {"username": "ada", "password": "correct horse"}. Sende POST /token mit demselben Body: Status 200 und {"token": "..."}. Kopiere das Token und sende GET /inspect?token=, gefolgt davon. Du siehst etwas wie {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp liegt 3600 Sekunden nach iat. Sende POST /token mit {"username": "ada", "password": "wrong horse"}: Status 401.

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) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// Die Admin-Seite. Keinem Claim kann vertraut werden, bevor seine Signatur überprüft wurde.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Alle Lektionen in Authentifizierung und Sicherheit

Übe selbstständig: Express-Playground