S SvelteKurs
M10 · Hooks, Auth & Praxis

Authentifizierung

Registrierung, Login und Sessions mit Cookies — sicher und ohne externe Library.

Die Bausteine

DevNotes nutzt eine klassische, selbstgebaute Session-Auth: bcrypt hasht Passwörter, eine sessions-Tabelle hält Tokens, und ein httpOnly-Cookie trägt das Token. Der handle-Hook (vorige Lektion) löst das Cookie bei jedem Request zum User auf.

🛤️ Rails-Vergleich: has_secure_password + session

Rails' has_secure_password nutzt intern bcrypt und authenticate. Wir bauen dasselbe explizit: hashPassword/verifyPassword plus ein Session-Cookie statt session[:user_id].

Rails ruby
# Rails: has_secure_password + Session-Cookie
class User < ApplicationRecord
  has_secure_password   # nutzt bcrypt unter der Haube
end

# SessionsController#create
user = User.find_by(username: params[:username])
if user&.authenticate(params[:password])
  session[:user_id] = user.id
  redirect_to notes_path
end

Das Auth-Modul

src/lib/server/auth.ts ts
import bcrypt from 'bcryptjs';
import { eq } from 'drizzle-orm';
import { db, schema } from './db';

export const SESSION_COOKIE = 'devnotes_session';

export const hashPassword = (pw: string) => bcrypt.hash(pw, 10);
export const verifyPassword = (pw: string, hash: string) => bcrypt.compare(pw, hash);

// Session-Zeile anlegen, Token zurückgeben (kommt ins Cookie):
export async function createSession(userId: number) {
  const token = crypto.randomUUID() + crypto.randomUUID().replace(/-/g, '');
  const expiresAt = new Date(Date.now() + 30 * 864e5); // 30 Tage
  await db.insert(schema.sessions).values({ id: token, userId, expiresAt });
  return { token, expiresAt };
}

// Token → User (abgelaufene Sessions werden gelöscht):
export async function validateSession(token?: string) {
  if (!token) return null;
  const [s] = await db.select().from(schema.sessions).where(eq(schema.sessions.id, token));
  if (!s || s.expiresAt.getTime() < Date.now()) return null;
  const [user] = await db.select().from(schema.users).where(eq(schema.users.id, s.userId));
  return user ?? null;
}

Registrierung

Die Form Action legt den User an, startet eine Session und setzt das Cookie:

src/routes/register/+page.server.ts ts
export const actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const username = String(data.get('username') ?? '').trim();
    const password = String(data.get('password') ?? '');

    if (password.length < 6) return fail(400, { error: 'Passwort zu kurz.' });

    const passwordHash = await hashPassword(password);
    const [user] = await db.insert(schema.users)
      .values({ username, passwordHash }).returning();

    const { token, expiresAt } = await createSession(user.id);
    cookies.set(SESSION_COOKIE, token, {
      path: '/', httpOnly: true, sameSite: 'lax', expires: expiresAt
    });
    throw redirect(303, '/devnotes');
  }
};

🚫 Passwörter NIE im Klartext

Speichere niemals das rohe Passwort. bcrypt.hash erzeugt einen Salt und einen langsamen Hash — genau das, was du willst. Beim Login vergleichst du mit bcrypt.compare, nie durch erneutes Hashen + String-Vergleich.

Login & Logout

Beim Login prüfst du Nutzer und Passwort — mit einer bewusst unspezifischen Fehlermeldung:

src/routes/login/+page.server.ts ts
const [user] = await db.select().from(schema.users)
  .where(eq(schema.users.username, username));

// Gleiche Fehlermeldung für "kein User" und "falsches Passwort" —
// so verrätst du nicht, welche Benutzernamen existieren.
if (!user || !(await verifyPassword(password, user.passwordHash))) {
  return fail(400, { error: 'Benutzername oder Passwort ist falsch.' });
}

Logout löscht die Session serverseitig und entfernt das Cookie:

ts
logout: async ({ cookies }) => {
  await deleteSession(cookies.get(SESSION_COOKIE));
  cookies.delete(SESSION_COOKIE, { path: '/' });
  throw redirect(303, '/login');
}

Warum httpOnly & sameSite?

ℹ️ Cookie-Sicherheit

  • httpOnly: JavaScript im Browser kann das Cookie nicht lesen → schützt vor XSS-Diebstahl.
  • sameSite: 'lax': schickt das Cookie nicht bei Cross-Site-POSTs → mildert CSRF.
  • In Produktion (HTTPS) setzt SvelteKit zusätzlich automatisch secure.

Zusammenfassung

💡 Das Wichtigste

  • Passwörter mit bcrypt hashen, mit compare prüfen.
  • Session-Token in einer Tabelle + httpOnly-Cookie; der Hook macht daraus locals.user.
  • Gleiche Fehlermeldung für falschen User/Passwort — keine Info-Lecks.

🎯 Probier es selbst

Registriere unter /register ein Konto und melde dich an. Öffne danach die Browser-DevTools → Application → Cookies und finde das devnotes_session-Cookie (als httpOnly markiert).