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.
Aufgabe
EinfachDieses 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.
- Schreibe in
server.jsGET /inspect. Die Route liest ein Token aus der Abfragezeichenfolge, wie inGET /inspect?token=eyJ..., und gibt die darin enthaltenen Angaben als JSON zurück. Lies sie mitdecodeJwtaus, das bereits importiert ist. Bei einem Token, dessen Payload{"sub": "lin", "plan": "free"}ist, gibt die Route genau{"sub": "lin", "plan": "free"}zurück. decodeJwtlö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ältGET /inspect?token=hellodiesen Status 400.- Sieh dir
GET /adminan. Die Route liest das Token mitdecodeJwtaus und zeigt ein Geheimnis an, wenn das Token"admin": trueaussagt. AberdecodeJwtprüft niemals die Signatur, daher kann jede Person ein Token eingeben, das"admin": trueaussagt. Dieser Server kann Signaturen noch nicht prüfen. Lass deshalbGET /adminbei 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');
});
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