Warum die meisten Angular-Projekte bei Sanctum landen
Die typische Architektur sieht so aus: ein hochstrukturiertes Angular-Frontend als SPA, dahinter Laravel als zustandslose API, die den Datenzugriff kontrolliert. Frontend und Backend sind strikt getrennt. Genau für dieses Szenario wurde Sanctum gebaut.
Der Reiz ist, dass Sie kein Token im Browser verwalten müssen. Stattdessen nutzt Sanctum das, was Laravel ohnehin kann: eine Session, gekoppelt an ein httpOnly-Cookie. Der Browser sendet dieses Cookie bei jeder Anfrage automatisch mit. Ihr Angular-Code muss es weder speichern noch anhängen. Das ist weniger Code und weniger Angriffsfläche.
Aus meiner Projekterfahrung scheitern viele Teams nicht an Sanctum selbst, sondern an einer falschen Erwartung: Sie versuchen, Sanctum wie ein Token-Auth-System zu behandeln und ein Bearer-Token im LocalStorage abzulegen. Das ist der Modus für mobile Apps und Third-Party-Clients, nicht der SPA-Modus. Für Angular gilt: Cookie-basiert, nicht Token-basiert. Wer das von Anfang an trennt, spart sich viel Frust.
Sanctum, Passport, JWT — wann welches Verfahren
Es gibt drei Verfahren, und jedes hat sein Szenario. Sie wählen nicht nach Geschmack, sondern nach Architektur.
Sanctum ist die Cookie-basierte SPA-Authentifizierung. Die Sicherheitsgewinne sind konkret: Das httpOnly-Cookie schützt vor XSS-Token-Diebstahl, weil JavaScript es nicht lesen kann. Der CSRF-Schutz läuft über den Endpunkt /sanctum/csrf-cookie und einen XSRF-TOKEN-Header. Voraussetzung: Frontend und API liegen same-site, also auf derselben Domain oder als Subdomains. Das ist der Standardfall für eine eigene Anwendung.
Passport ist die schwere Maschine: ein vollwertiger OAuth2-Server. Sie brauchen ihn, wenn fremde Clients auf Ihre API zugreifen sollen, mit Authorization-Code-Flow oder Client-Credentials-Flow. Für eine reine First-Party-SPA ist Passport überdimensioniert.
JWT ist die zustandslose Variante. Sinnvoll, wenn Sie über getrennte Domains oder zwischen Microservices authentifizieren und bewusst keine serverseitige Session halten wollen.
Die Faustregel, die ich Kunden mitgebe:
- First-Party-SPA → Sanctum
- OAuth2-Plattform mit Drittanbietern → Passport
- Stateless cross-service → JWT
Der CSRF-Tanz: wie der Sanctum-Login wirklich abläuft
Der Punkt, an dem die meisten hängenbleiben, ist die Reihenfolge. Sanctum verlangt, dass Sie zuerst ein CSRF-Cookie abholen, bevor Sie eine zustandsändernde Anfrage wie Login schicken. Konkret:
Erstens ruft Angular GET /sanctum/csrf-cookie auf. Laravel setzt daraufhin ein XSRF-TOKEN-Cookie. Zweitens schickt Angular die Login-Anfrage. Der HttpClient liest das XSRF-TOKEN-Cookie und sendet seinen Wert als X-XSRF-TOKEN-Header zurück. Laravel vergleicht beide und lässt die Anfrage nur durch, wenn sie übereinstimmen. So wird sichergestellt, dass die Anfrage tatsächlich von Ihrer SPA stammt.
Auf der Laravel-Seite ist wenig nötig. Die Konfiguration für die Stateful-Domains und der Auth-Endpunkt:
// config/sanctum.php
'stateful' => explode(',', env(
'SANCTUM_STATEFUL_DOMAINS',
'localhost,localhost:4200,127.0.0.1'
)),
// routes/web.php (web-Gruppe, damit Session + CSRF greifen)
Route::post('/login', function (Request $request) {
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
return response()->json(['message' => 'Ungültige Zugangsdaten'], 422);
}
$request->session()->regenerate();
return response()->json(['user' => Auth::user()]);
});
Wichtig: Der Login-Endpunkt gehört in die web-Middleware-Gruppe, nicht in api. Nur dort greifen Session und CSRF-Schutz. Geschützte Routen sichern Sie anschließend mit dem auth:sanctum-Guard ab.
// routes/api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Angular richtig verdrahten: withCredentials und Interceptor
Auf der Angular-Seite sind zwei Dinge entscheidend. Erstens muss jede Anfrage withCredentials: true setzen, sonst sendet der Browser das Cookie nicht und nimmt es auch nicht an. Zweitens kapseln Sie den HTTP-Zugriff in einer dedizierten Service-Klasse, nicht in der Komponente. Den Datenzugriff in den Komponenten zu verstreuen ist einer der häufigeren Architekturfehler.
Angulars HttpClient erkennt das XSRF-TOKEN-Cookie und spiegelt es automatisch in den X-XSRF-TOKEN-Header, sofern Sie das einrichten. Ein Interceptor sorgt dafür, dass withCredentials global gesetzt ist und Auth-Fehler zentral behandelt werden.
// auth.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private base = 'https://api.example.de';
readonly currentUser = signal<User | null>(null);
async login(email: string, password: string): Promise<void> {
// 1. CSRF-Cookie abholen
await firstValueFrom(
this.http.get(`${this.base}/sanctum/csrf-cookie`, { withCredentials: true })
);
// 2. Login — HttpClient haengt X-XSRF-TOKEN automatisch an
const res = await firstValueFrom(
this.http.post<{ user: User }>(
`${this.base}/login`,
{ email, password },
{ withCredentials: true }
)
);
this.currentUser.set(res.user);
}
}
Damit der HttpClient das CSRF-Cookie überhaupt spiegelt und withCredentials standardmäßig setzt, konfigurieren Sie ihn beim Bootstrapping. Ein eigener Interceptor erzwingt withCredentials und fängt 401-Antworten ab.
// app.config.ts
import { provideHttpClient, withInterceptors, withXsrfConfiguration }
from '@angular/common/http';
import { credentialsInterceptor } from './credentials.interceptor';
export const appConfig = {
providers: [
provideHttpClient(
withInterceptors([credentialsInterceptor]),
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN',
})
),
],
};
// credentials.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
export const credentialsInterceptor: HttpInterceptorFn = (req, next) => {
const authReq = req.clone({ withCredentials: true });
return next(authReq).pipe(
catchError((err) => {
if (err.status === 401) {
// zentrale Behandlung: z. B. Redirect auf Login
}
return throwError(() => err);
})
);
};
Beachten Sie das withXsrfConfiguration mit Cookie-Name XSRF-TOKEN und Header-Name X-XSRF-TOKEN. Das ist exakt das Namenspaar, das Sanctum erwartet. Stimmen die Namen nicht, schlägt jede schreibende Anfrage mit einem 419-Fehler fehl. Das ist die Nummer eins der Sanctum-Tickets, die ich sehe.
XSS und CSRF: was Sanctum abdeckt und was nicht
Sicherheit ist der eigentliche Grund, warum ich für SPAs den Cookie-Weg empfehle. Zwei Bedrohungen stehen im Vordergrund.
Gegen CSRF schützt der beschriebene Token-Flow. Ein Angreifer auf einer fremden Seite kann zwar eine Anfrage an Ihre API auslösen, aber er kann das XSRF-TOKEN-Cookie nicht auslesen und damit den passenden Header nicht setzen. Ohne gültigen Header weist Laravel die Anfrage ab.
Gegen XSS-Token-Diebstahl schützt das httpOnly-Flag. Würden Sie ein JWT im LocalStorage halten, könnte jeder erfolgreiche XSS-Angriff dieses Token auslesen und die Sitzung übernehmen. Ein httpOnly-Cookie ist für JavaScript unsichtbar. Selbst eingeschleuster Code kommt nicht an den Session-Schlüssel.
Mein Rat: Verlassen Sie sich trotzdem nicht allein auf httpOnly. XSS bleibt gefährlich, weil eingeschleuster Code im Namen des eingeloggten Nutzers Anfragen absetzen kann, auch ohne das Cookie zu sehen. Saubere Output-Kodierung, eine Content-Security-Policy und Angulars eingebaute Sanitization gehören dazu. Und auf der API-Seite: Lecken Sie keine internen Modelle. Nutzen Sie API Resources, um Eloquent-Modelle kontrolliert in JSON zu überführen, statt das Modell eins zu eins durchzureichen.
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// bewusst kein password_hash, kein internes Feld
];
}
}
Halten Sie zudem die Controller dünn. Validierung gehört in FormRequest-Klassen, Geschäftslogik in Service-Klassen. Ein Controller mit 10 bis 15 Zeilen, der nur orchestriert, ist leichter zu prüfen und schwerer zu kompromittieren als einer, der alles selbst macht.
Der Vertrag zwischen Backend und Frontend
Ein Detail, das Authentifizierung und Datenfluss verbindet: Typisieren Sie jede API-Antwort. Kein any. Die Login-Antwort, das Nutzerobjekt, alles bekommt ein TypeScript-Interface. Das User-Interface oben ist nicht Kosmetik. Es ist der Vertrag zwischen Ihrer Laravel-Resource und dem Angular-Code.
Der Nutzen ist praktisch: Der Compiler fängt Tippfehler bei Attributnamen, die Autovervollstaendigung wird deutlich besser, und der Code dokumentiert sich selbst. Wer das Interface liest, kennt die Datenstruktur, ohne in die Datenbank zu schauen. Genau hier zahlt sich die strikte Trennung von Angular und Laravel aus.
Wer tiefer in das Zusammenspiel einsteigen will: Im Angular + Laravel Leitfaden ordne ich die Architektur insgesamt ein. Wie Sie API Resources sauber in TypeScript-Interfaces übersetzen, steht in Laravel API Resources und Angular-Typsicherheit. Und warum ich Auth-Status inzwischen oft in Signals statt in RxJS-Subjects halte, lesen Sie in State-Management mit Angular Signals.
Sicheren Auth-Flow für Ihr Projekt aufsetzen
Authentifizierung ist der Teil, bei dem kleine Fehler große Folgen haben. Wenn Sie eine Angular-plus-Laravel-Anwendung planen oder einen bestehenden Auth-Flow härten wollen, schauen wir gemeinsam drauf. Schreiben Sie mir über das Kontaktformular — ich melde mich mit einer konkreten Einschätzung.
FAQ
Häufige Fragen
Sanctum oder Passport für meine Angular-SPA?
Sanctum, sofern es Ihre eigene Anwendung ist und keine fremden Clients zugreifen. Passport ist ein vollwertiger OAuth2-Server und nur dann nötig, wenn Sie Authorization-Code- oder Client-Credentials-Flows für Drittanbieter brauchen. Für die meisten First-Party-SPAs ist Passport zu schwer.
Warum bekomme ich beim Login einen 419-Fehler?
419 bedeutet fehlender oder ungültiger CSRF-Token. Prüfen Sie drei Dinge: Holen Sie vor dem Login GET /sanctum/csrf-cookie ab? Ist withCredentials: true gesetzt? Stimmen Cookie-Name (XSRF-TOKEN) und Header-Name (X-XSRF-TOKEN) in Ihrer Angular-Konfiguration mit dem überein, was Sanctum erwartet? In aller Regel liegt es an einem dieser Punkte.
Muss das Angular-Frontend auf derselben Domain wie die API liegen?
Für den Cookie-Modus von Sanctum müssen Frontend und API same-site sein, also dieselbe Domain oder Subdomains derselben Top-Level-Domain. Reine Cross-Origin-Setups über völlig getrennte Domains passen nicht zum Cookie-Modus. In dem Fall ist ein zustandsloses Verfahren wie JWT die ehrlichere Wahl.
Soll ich das Token im LocalStorage speichern?
Im SPA-Modus von Sanctum gibt es kein Token, das Sie selbst speichern. Die Sitzung lebt im httpOnly-Cookie, das der Browser automatisch verwaltet. Ein Token im LocalStorage abzulegen wäre genau der XSS-anfällige Weg, den der Cookie-Ansatz vermeidet.
Wann ist JWT die bessere Wahl als Sanctum?
Wenn Sie zustandslos über getrennte Domains oder zwischen Microservices authentifizieren und bewusst keine serverseitige Session führen wollen. Sobald Ihr Frontend und Ihre Services nicht mehr same-site sind oder mehrere unabhängige Backends ein Token akzeptieren sollen, spielt JWT seine Stärke aus.
Wie schütze ich geschützte Routen in Angular zusätzlich?
Setzen Sie Route-Guards ein, die den Auth-Status prüfen, und behandeln Sie 401-Antworten zentral im Interceptor. Der Guard verhindert die Navigation im Frontend, der Interceptor reagiert auf die Backend-Antwort. Beides zusammen ergibt eine konsistente Absicherung. Die eigentliche Autorisierung bleibt aber immer Aufgabe von Laravel.
