Ablauf von Tokens
Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 17 von 24.
Ein Server kann ein Token nicht zurücknehmen: Es wird nichts gespeichert, also funktioniert ein gestohlenes Token, bis es abläuft. Halte die Gültigkeitsdauer von Tokens kurz, eher Minuten als Tage, und lass den Client ein neues anfordern.
jwtVerify weist ein Token, dessen exp abgelaufen ist, bereits zurück. exp ist ein Zeitpunkt, gezählt in Sekunden seit dem 1. Januar 1970. Dabei wird ein errors.JWTExpired ausgelöst. err instanceof errors.JWTExpired ist bei dieser Art von Fehler wahr, sodass du ein abgelaufenes Token von einem gefälschten unterscheiden und das auch mitteilen kannst: Ein Client, der Token abgelaufen liest, weiß, dass er sich erneut anmelden muss.
import { errors, jwtVerify } from 'jose';
try {
await jwtVerify(inviteToken, inviteKey);
} catch (err) {
if (err instanceof errors.JWTExpired) {
return res.status(410).json({ error: 'This invite has run out' });
}
return res.status(400).json({ error: 'Not a real invite' });
}Aufgabe
EinfachEin Server kann ein Token nicht zurücknehmen. Ein gestohlenes Token funktioniert also weiter, bis es abläuft. Lass Tokens nach kurzer Zeit ablaufen und teile einer App klar mit, wenn ihr Token abgelaufen ist, damit sie weiß, dass sie sich erneut anmelden muss.
- Lege in
server.jsbeiPOST /tokenfest, dass Tokens 15 Minuten nach ihrer Erstellung ablaufen, statt nach einer Stunde. - Sieh dir in
requireUserden Fehler an, denjwtVerifyauslöst. Bei einem abgelaufenen Token löstjoseeinen Fehler vom Typerrors.JWTExpiredaus. Importiereerrorsausjose. Wenn es sich um diesen Fehlertyp handelt, antworte mit Status 401 und{"error": "Token expired"}. - Bei allen anderen Problemen bleibt es bei Status 401 und
{"error": "Log in first"}: kein Header, ein geändertes Token oder ein mit einem anderen Schlüssel signiertes Token.
Probiere es aus: Drücke auf „Spielen“. Sende POST /signup und anschließend POST /token, beide mit {"username": "ada", "password": "correct horse"}. Sende GET /inspect?token= gefolgt vom neuen Token: exp liegt nun 900 Sekunden nach iat. Beide Werte zählen die Sekunden seit dem 1. Januar 1970. Hier ist nun ein Token, das mit dem geheimen Schlüssel dieses Servers signiert wurde und am 1. Januar 2026 abgelaufen ist: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI2NTAwfQ.PL8VCHpGNwBJ-7EawZ60veEEE-ZyosNC7xYQQpgT2uE Setze im Feld „Headers“ Authorization auf Bearer, ein Leerzeichen und dieses Token, und sende dann GET /me: Status 401 und {"error": "Token expired"}. Ändere einen Buchstaben im mittleren Teil des Tokens und sende die Anfrage erneut: Status 401 und {"error": "Log in first"}. Bei einem geänderten Token schlägt die Signaturprüfung fehl, bevor sein Ablaufdatum gelesen wird.
Probier es selbst
import express from 'express';
import { SignJWT, decodeJwt, jwtVerify } 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 });
});
// Prüft das Token im Authorization-Header oder antwortet mit 401.
async function requireUser(req, res, next) {
const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
let payload;
try {
({ payload } = await jwtVerify(token, secret));
} catch {
return res.status(401).json({ error: 'Log in first' });
}
req.user = { id: payload.sub, username: payload.username };
next();
}
app.get('/me', requireUser, (req, res) => {
res.json(req.user);
});
// 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 geprü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