Menu
CoddyTech

Tokens verifizieren

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

Ein API-Client sendet sein Token bei jeder Anfrage in einem Header mit: Authorization: Bearer <token>. Wenn du es mit decodeJwt ausliest, würdest du jedes Token akzeptieren, das jemand eingegeben hat. jwtVerify(token, key) berechnet die Signatur mit deinem geheimen Schlüssel neu und löst einen Fehler aus, wenn sie nicht übereinstimmt, wenn das Token abgelaufen ist oder wenn es überhaupt kein JWT ist. Erst wenn die Funktion zurückkehrt, darfst du payload vertrauen.

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

Aufgabe

Einfach

Akzeptiere jetzt die von dir signierten Tokens. Eine App sendet ihr Token bei jeder Anfrage in einem Header, und der Server prüft die Signatur, bevor er irgendetwas darin vertraut.

  1. Importiere in server.js jwtVerify aus jose.
  2. Schreibe eine Middleware requireUser(req, res, next). Sie liest den Authorization-Header. Dieser Header enthält das Wort Bearer, ein Leerzeichen und anschließend das Token. Hole das Token daraus heraus.
  3. Prüfe das Token mit jwtVerify und verwende dabei denselben Schlüssel, mit dem du signierst. jwtVerify löst einen Fehler aus, wenn die Signatur nicht übereinstimmt, wenn das Token abgelaufen ist oder wenn der Text überhaupt kein Token ist. Antworte in all diesen Fällen und auch dann, wenn kein Header vorhanden ist, mit Status 401 und {"error": "Log in first"}.
  4. Wenn das Token gültig ist, setze req.user auf ein Objekt mit id, das aus dem sub-Claim des Tokens stammt, und username, das aus dessen username-Claim stammt. Rufe dann next() auf.
  5. Füge GET /me mit requireUser hinzu. Es gibt req.user zurück: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

Probiere es aus: Drücke auf „Ausführen“. Sende GET /me: Status 401 und {"error": "Log in first"}. Sende POST /signup und dann POST /token, jeweils mit {"username": "ada", "password": "correct horse"}, und kopiere das Token. Füge im Feld „Headers“ einen Header mit dem Namen Authorization hinzu. Sein Wert ist Bearer, ein Leerzeichen und dein Token. Sende GET /me: Status 200, mit deiner ID und "username": "ada". Ändere nun einen Buchstaben in der Mitte des Tokens, zwischen den beiden Punkten, und sende erneut GET /me: Status 401, weil die Signatur nicht mehr übereinstimmt.

Probier es selbst

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

// Zeigt den Inhalt eines Tokens an: 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. Keiner Angabe darf vertraut werden, bevor ihre 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');
});
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