Kodokon kodokon.com

Authentifizierung: Hashing, Sessions vs. JWT

Hashe Passwörter mit einer teuren Funktion, wähle bewusst zwischen Sessions und JWT und schütze deine Routen mit einer Middleware.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Ein Passwort zu speichern heißt, sich auf einen künftigen Einbruch vorzubereiten: Gestalte das Hashing so, dass es den Diebstahl der Datenbank überlebt. Ein schneller Hash wie SHA-256 lässt sich auf einer GPU mit Milliarden Versuchen pro Sekunde durchprobieren. Du brauchst eine absichtlich teure Funktion - scrypt, das node:crypto mitbringt, oder bcrypt/argon2 - und ein zufälliges Salt pro Nutzer, das vorberechnete Tabellen nutzlos macht. Das Salt ist kein Geheimnis: Es wird im Klartext neben dem Hash gespeichert.

JAVASCRIPT
import {
  randomBytes,
  scryptSync,
  timingSafeEqual,
} from 'node:crypto';

export function hashPassword(password) {
  const salt = randomBytes(16).toString('hex');
  const hash = scryptSync(password, salt, 64)
    .toString('hex');
  return `${salt}:${hash}`;
}

export function verifyPassword(password, stored) {
  const [salt, hash] = stored.split(':');
  const candidate = scryptSync(password, salt, 64);
  const expected = Buffer.from(hash, 'hex');
  return timingSafeEqual(candidate, expected);
}
Zufälliges Salt, teure Ableitung, Vergleich in konstanter Zeit.

Zwei Modelle, um den Nutzer angemeldet zu halten. Die Session: eine undurchsichtige Kennung in einem httpOnly-Cookie, mit dem Zustand auf Server-Seite. Sofortiger Entzug, Cookie für JavaScript unsichtbar, aber ein gemeinsamer Speicher (Redis) wird nötig, sobald du eine zweite Instanz hinzufügst. Das JWT: der signierte Zustand steckt im Token, überprüfbar ohne jeden Speicher - ideal zwischen Diensten - aber vor Ablauf nicht widerrufbar. Der ehrliche Kompromiss: Sessions für eine klassische Webanwendung, kurzlebige JWTs für APIs, die von Dritten genutzt werden, oder für verteilte Architekturen.

JAVASCRIPT
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET;
if (!SECRET) {
  throw new Error('JWT_SECRET must be set');
}

export function signToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '15m' },
  );
}
Secret beim Start erforderlich, Tokens mit kurzer Lebensdauer.

Die Schutz-Middleware lagert die Zugriffskontrolle aus: Sie zieht das Token aus dem Header Authorization: Bearer, prüft es, hängt die dekodierte Identität an req.user oder macht mit einem 401 Schluss, das an die Fehler-Middleware der vorherigen Lektion geleitet wird. Wende sie auf Router-Ebene an (router.use(requireAuth)), um eine ganze Gruppe von Routen zu schützen, statt Route für Route.

JAVASCRIPT
import jwt from 'jsonwebtoken';
import { HttpError } from './http-error.js';

const SECRET = process.env.JWT_SECRET;

export function requireAuth(req, res, next) {
  const header = req.headers.authorization ?? '';
  const [scheme, token] = header.split(' ');
  if (scheme !== 'Bearer' || !token) {
    return next(new HttpError(401, 'Missing token'));
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    next(new HttpError(401, 'Invalid or expired token'));
  }
}
Prüfen, req.user anhängen oder 401 - nichts anderes.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum eignet sich SHA-256 allein nicht zum Speichern von Passwörtern?
    • Weil der Algorithmus kryptografisch gebrochen ist
    • Weil er zu schnell ist: Eine GPU testet Milliarden Kandidaten pro Sekunde; du brauchst eine langsame Funktion mit Salt
    • Weil er bei kurzen Passwörtern Kollisionen erzeugt
    • Weil Node ihn nicht nativ implementiert
  2. Welcher Mechanismus erlaubt es, einen Zugang sofort zu entziehen, noch vor jedem Ablauf?
    • Das JWT, weil es sein Ablaufdatum mitführt
    • Die serverseitige Session, weil du einfach ihren Eintrag aus dem Speicher löschst
    • Beide gleichermaßen
  3. Wofür ist timingSafeEqual bei der Passwortprüfung da?
    • Um den Vergleich der beiden Hashes zu beschleunigen
    • Um in konstanter Zeit zu vergleichen, damit die Antwortdauer nicht verrät, wie viele Bytes übereinstimmen
    • Um die hexadezimalen Hashes in Buffer umzuwandeln