22 Authentifizierung in Angular

Die Authentifizierung ist ein kritischer Bestandteil moderner Webanwendungen und dient dazu, die Identität von Benutzern zu verifizieren, bevor ihnen Zugriff auf geschützte Ressourcen gewährt wird. Die Best Practices für Authentifizierung entwickeln sich ständig weiter, während die Grundprinzipien erhalten bleiben.

22.1 Moderne Authentifizierungsstrategien

  1. Token-basierte Authentifizierung: Der De-facto-Standard für moderne Webanwendungen:
  2. OAuth 2.0 & OpenID Connect:
  3. Passwortlose Authentifizierung:
  4. Multi-Faktor-Authentifizierung (MFA):

22.2 Implementierung in Angular

22.2.1 Authentication Service mit modernem State Management

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';

export interface AuthState {
  user: any | null;
  accessToken: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  private jwtHelper = inject(JwtHelperService);
  
  private readonly API_URL = 'https://api.example.com/auth';
  private readonly TOKEN_KEY = 'auth_tokens';
  
  // Modern reactive state management 
  private authStateSubject = new BehaviorSubject<AuthState>({
    user: null,
    accessToken: null,
    refreshToken: null,
    isAuthenticated: false
  });
  
  authState$ = this.authStateSubject.asObservable();
  
  constructor() {
    this.initializeFromStorage();
    this.setupTokenRefresh();
  }
  
  private initializeFromStorage(): void {
    const storedTokens = localStorage.getItem(this.TOKEN_KEY);
    if (storedTokens) {
      try {
        const { accessToken, refreshToken } = JSON.parse(storedTokens);
        if (accessToken && !this.jwtHelper.isTokenExpired(accessToken)) {
          const user = this.jwtHelper.decodeToken(accessToken);
          this.authStateSubject.next({
            user,
            accessToken,
            refreshToken,
            isAuthenticated: true
          });
        } else if (refreshToken) {
          // Silent token refresh on app initialization
          this.refreshAccessToken(refreshToken).subscribe();
        }
      } catch (error) {
        console.error('Failed to parse stored tokens', error);
        localStorage.removeItem(this.TOKEN_KEY);
      }
    }
  }
  
  login(credentials: { email: string; password: string }): Observable<AuthState> {
    return this.http.post<{accessToken: string; refreshToken: string}>(
      `${this.API_URL}/login`, 
      credentials
    ).pipe(
      tap(response => this.handleAuthResponse(response)),
      map(() => this.authStateSubject.value),
      catchError(error => {
        console.error('Login failed', error);
        return throwError(() => new Error('Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.'));
      })
    );
  }
  
  loginWithProvider(provider: 'google' | 'facebook' | 'github'): void {
    // Redirect to OAuth provider
    window.location.href = `${this.API_URL}/oauth/${provider}`;
  }
  
  handleRedirectCallback(code: string): Observable<AuthState> {
    return this.http.get<{accessToken: string; refreshToken: string}>(
      `${this.API_URL}/oauth/callback?code=${code}`
    ).pipe(
      tap(response => this.handleAuthResponse(response)),
      map(() => this.authStateSubject.value)
    );
  }
  
  logout(): void {
    // Optional: notify backend about logout
    this.http.post(`${this.API_URL}/logout`, {
      refreshToken: this.authStateSubject.value.refreshToken
    }).subscribe({
      next: () => this.completeLogout(),
      error: () => this.completeLogout()
    });
  }
  
  private completeLogout(): void {
    localStorage.removeItem(this.TOKEN_KEY);
    this.authStateSubject.next({
      user: null,
      accessToken: null,
      refreshToken: null,
      isAuthenticated: false
    });
    this.router.navigate(['/login']);
  }
  
  private handleAuthResponse(response: {accessToken: string; refreshToken: string}): void {
    const { accessToken, refreshToken } = response;
    const user = this.jwtHelper.decodeToken(accessToken);
    
    // Update state
    this.authStateSubject.next({
      user,
      accessToken,
      refreshToken,
      isAuthenticated: true
    });
    
    // Persist tokens securely
    localStorage.setItem(this.TOKEN_KEY, JSON.stringify({ accessToken, refreshToken }));
  }
  
  refreshAccessToken(refreshToken?: string): Observable<AuthState> {
    const tokenToUse = refreshToken || this.authStateSubject.value.refreshToken;
    
    if (!tokenToUse) {
      return throwError(() => new Error('Kein Refresh-Token verfügbar'));
    }
    
    return this.http.post<{accessToken: string; refreshToken: string}>(
      `${this.API_URL}/refresh`,
      { refreshToken: tokenToUse }
    ).pipe(
      tap(response => this.handleAuthResponse(response)),
      map(() => this.authStateSubject.value),
      catchError(error => {
        console.error('Token refresh failed', error);
        this.completeLogout();
        return throwError(() => new Error('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.'));
      })
    );
  }
  
  private setupTokenRefresh(): void {
    // Setup automatic token refresh before expiration
    setInterval(() => {
      const { accessToken, refreshToken } = this.authStateSubject.value;
      
      if (accessToken && refreshToken) {
        const expirationDate = this.jwtHelper.getTokenExpirationDate(accessToken);
        const now = new Date();
        
        // Refresh if token expires in less than 5 minutes
        if (expirationDate && expirationDate.getTime() - now.getTime() < 300000) {
          this.refreshAccessToken().subscribe();
        }
      }
    }, 60000); // Check every minute
  }
  
  // Convenience getters
  get isAuthenticated(): boolean {
    return this.authStateSubject.value.isAuthenticated;
  }
  
  get currentUser(): any | null {
    return this.authStateSubject.value.user;
  }
  
  get accessToken(): string | null {
    return this.authStateSubject.value.accessToken;
  }
}

22.2.2 Moderner Auth Interceptor mit automatischem Token Refresh

import { Injectable, inject } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private authService = inject(AuthService);
  private isRefreshing = false;
  private refreshTokenSubject = new BehaviorSubject<string | null>(null);
  
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Skip auth header for public endpoints
    if (request.url.includes('/auth/login') || request.url.includes('/auth/refresh')) {
      return next.handle(request);
    }
    
    // Add auth header if user is authenticated
    const accessToken = this.authService.accessToken;
    if (accessToken) {
      request = this.addToken(request, accessToken);
    }
    
    return next.handle(request).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return this.handle401Error(request, next);
        }
        return throwError(() => error);
      })
    );
  }
  
  private addToken(request: HttpRequest<any>, token: string): HttpRequest<any> {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
  
  private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);
      
      return this.authService.refreshAccessToken().pipe(
        switchMap(state => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(state.accessToken);
          return next.handle(this.addToken(request, state.accessToken!));
        }),
        catchError(error => {
          this.isRefreshing = false;
          this.authService.logout();
          return throwError(() => error);
        })
      );
    } else {
      // Wait until token is refreshed
      return this.refreshTokenSubject.pipe(
        filter(token => token !== null),
        take(1),
        switchMap(token => next.handle(this.addToken(request, token!)))
      );
    }
  }
}

22.2.3 Moderne Route Guards mit Standalone API

Neuere Angular-Versionen nutzen funktionale Route Guards. Hier ein moderner Ansatz:

import { inject } from '@angular/core';
import { Router, CanActivateFn, UrlTree } from '@angular/router';
import { AuthService } from './auth.service';
import { map, take, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';

// Functional Auth Guard
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  return authService.authState$.pipe(
    take(1),
    map(authState => authState.isAuthenticated),
    tap(isAuthenticated => {
      if (!isAuthenticated) {
        // Store attempted URL for redirection after login
        router.navigate(['/login'], { 
          queryParams: { returnUrl: state.url }
        });
      }
    })
  );
};

// Role-based Auth Guard
export const roleGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  const requiredRole = route.data['requiredRole'] as string;
  
  return authService.authState$.pipe(
    take(1),
    map(authState => {
      const hasRequiredRole = authState.user?.roles?.includes(requiredRole);
      
      if (!authState.isAuthenticated) {
        // Navigate to login
        router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
      }
      
      if (!hasRequiredRole) {
        // Navigate to unauthorized page
        router.navigate(['/unauthorized']);
        return false;
      }
      
      return true;
    })
  );
};

22.2.4 Verwendung im Standalone-Setup

// main.ts (Standalone Application Setup)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideJwtHelperService } from './app/auth/jwt-helper.provider';
import { AppComponent } from './app/app.component';
import { appRoutes } from './app/app.routes';
import { authInterceptor } from './app/auth/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      appRoutes,
      withComponentInputBinding()
    ),
    provideHttpClient(
      withInterceptors([authInterceptor])
    ),
    provideJwtHelperService()
  ]
}).catch(err => console.error(err));

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard, roleGuard } from './auth/auth.guards';
import { LoginComponent } from './auth/login.component';
import { ProfileComponent } from './profile/profile.component';
import { AdminDashboardComponent } from './admin/admin-dashboard.component';

export const appRoutes: Routes = [
  { path: '', redirectTo: 'profile', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { 
    path: 'profile', 
    component: ProfileComponent,
    canActivate: [authGuard]
  },
  { 
    path: 'admin', 
    component: AdminDashboardComponent,
    canActivate: [roleGuard],
    data: { requiredRole: 'ADMIN' }
  }
];

22.2.5 Login-Komponente mit modernem Angular Syntax

// login.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from '../auth/auth.service';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [CommonModule, FormsModule, ReactiveFormsModule],
  template: `
    <div class="login-container">
      <h2>Anmelden</h2>
      
      <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="email">E-Mail</label>
          <input 
            type="email" 
            id="email" 
            formControlName="email" 
            class="form-control"
            [class.is-invalid]="isFieldInvalid('email')"
          >
          @if (isFieldInvalid('email')) {
            <div class="invalid-feedback">
              @if (emailControl.errors?.['required']) {
                Bitte geben Sie Ihre E-Mail-Adresse ein.
              } @else if (emailControl.errors?.['email']) {
                Bitte geben Sie eine gültige E-Mail-Adresse ein.
              }
            </div>
          }
        </div>
        
        <div class="form-group">
          <label for="password">Passwort</label>
          <input 
            type="password" 
            id="password" 
            formControlName="password" 
            class="form-control"
            [class.is-invalid]="isFieldInvalid('password')"
          >
          @if (isFieldInvalid('password')) {
            <div class="invalid-feedback">
              Bitte geben Sie Ihr Passwort ein.
            </div>
          }
        </div>
        
        <div class="form-actions">
          <button 
            type="submit" 
            class="btn btn-primary" 
            [disabled]="loginForm.invalid || isLoading"
          >
            @if (isLoading) {
              <span class="spinner-border spinner-border-sm me-2"></span>
            }
            Anmelden
          </button>
        </div>
      </form>
      
      <div class="social-login">
        <p>Oder anmelden mit:</p>
        <div class="btn-group">
          <button 
            type="button" 
            class="btn btn-outline-secondary" 
            (click)="loginWithProvider('google')"
          >
            Google
          </button>
          <button 
            type="button" 
            class="btn btn-outline-secondary" 
            (click)="loginWithProvider('github')"
          >
            GitHub
          </button>
        </div>
      </div>
      
      @if (errorMessage) {
        <div class="alert alert-danger mt-3">
          {{ errorMessage }}
        </div>
      }
    </div>
  `,
  styles: [`
    .login-container {
      max-width: 400px;
      margin: 2rem auto;
      padding: 2rem;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }
    
    .form-group {
      margin-bottom: 1.5rem;
    }
    
    .social-login {
      margin-top: 2rem;
      text-align: center;
    }
  `]
})
export class LoginComponent {
  private fb = inject(FormBuilder);
  private authService = inject(AuthService);
  private router = inject(Router);
  private route = inject(ActivatedRoute);
  
  loginForm: FormGroup = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', Validators.required]
  });
  
  isLoading = false;
  errorMessage: string | null = null;
  
  get emailControl() {
    return this.loginForm.get('email')!;
  }
  
  isFieldInvalid(fieldName: string): boolean {
    const control = this.loginForm.get(fieldName);
    return !!control && control.invalid && (control.dirty || control.touched);
  }
  
  onSubmit(): void {
    if (this.loginForm.invalid) {
      // Markiere alle Felder als berührt, um Validierungsfehler anzuzeigen
      Object.keys(this.loginForm.controls).forEach(key => {
        this.loginForm.get(key)?.markAsTouched();
      });
      return;
    }
    
    this.isLoading = true;
    this.errorMessage = null;
    
    this.authService.login(this.loginForm.value).subscribe({
      next: () => {
        const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
        this.router.navigateByUrl(returnUrl);
      },
      error: (error) => {
        this.errorMessage = error.message || 'Anmeldung fehlgeschlagen. Bitte versuchen Sie es später erneut.';
        this.isLoading = false;
      }
    });
  }
  
  loginWithProvider(provider: 'google' | 'github'): void {
    this.authService.loginWithProvider(provider);
  }
}

22.3 CSRF Protection in Angular

Cross-Site Request Forgery (CSRF) ist ein Angriff, bei dem ein Angreifer einen authentifizierten Benutzer dazu bringt, ungewollte Aktionen auf einer Webanwendung auszuführen. Angular bietet eingebaute Mechanismen zum Schutz vor CSRF-Angriffen, die in modernen Anwendungen implementiert werden sollten.

22.3.1 CSRF-Grundlagen für Angular-Entwickler

CSRF-Angriffe nutzen die Tatsache aus, dass Browser automatisch Cookies mit Anfragen senden. Ein Angreifer kann eine gefälschte Anfrage von einer anderen Website aus senden, die im Namen des authentifizierten Benutzers ausgeführt wird:

// Beispiel eines CSRF-Angriffs (vereinfacht)
// Von einer bösartigen Website:
fetch('https://banking-app.com/transfer', {
  method: 'POST',
  credentials: 'include', // Sendet Cookies automatisch mit
  body: JSON.stringify({
    to: 'attacker-account',
    amount: 1000
  })
});

22.3.2 Angular’s eingebauter CSRF-Schutz

Angular implementiert das “Double Submit Cookie”-Pattern für CSRF-Schutz:

// main.ts - Moderne Standalone-Konfiguration mit CSRF-Schutz
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',           // Name des CSRF-Cookies
        headerName: 'X-XSRF-TOKEN'          // Name des CSRF-Headers
      })
    )
  ]
}).catch(err => console.error(err));

22.3.3 CSRF-Token-Handling in Services

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CsrfAwareApiService {
  private http = inject(HttpClient);
  
  /**
   * POST-Request mit automatischem CSRF-Schutz
   * Angular fügt automatisch den X-XSRF-TOKEN Header hinzu
   */
  createUser(userData: any): Observable<any> {
    // Angular erkennt automatisch state-changing Methoden (POST, PUT, DELETE)
    // und fügt den CSRF-Token-Header hinzu
    return this.http.post('/api/users', userData);
  }
  
  /**
   * Explizite CSRF-Token-Behandlung für spezielle Fälle
   */
  performSensitiveAction(data: any): Observable<any> {
    // Manchmal ist explizite Kontrolle erforderlich
    const csrfToken = this.getCsrfTokenFromCookie();
    
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
      'X-XSRF-TOKEN': csrfToken || ''
    });
    
    return this.http.post('/api/sensitive-action', data, { headers });
  }
  
  /**
   * CSRF-Token aus Cookie lesen (falls manuelle Behandlung erforderlich)
   */
  private getCsrfTokenFromCookie(): string | null {
    const cookies = document.cookie.split(';');
    const xsrfCookie = cookies.find(cookie => 
      cookie.trim().startsWith('XSRF-TOKEN=')
    );
    
    return xsrfCookie ? 
      decodeURIComponent(xsrfCookie.split('=')[1]) : 
      null;
  }
}

22.3.4 CSRF-Schutz in Authentication-Flows

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, tap } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SecureAuthService {
  private http = inject(HttpClient);
  private readonly API_URL = 'https://api.example.com/auth';
  
  /**
   * Login mit CSRF-Schutz
   */
  login(credentials: { email: string; password: string }): Observable<any> {
    // Für Login-Requests ist oft ein initial CSRF-Token erforderlich
    return this.obtainCsrfToken().pipe(
      switchMap(() => {
        // Nach dem Abrufen des CSRF-Tokens wird automatisch der Header gesetzt
        return this.http.post(`${this.API_URL}/login`, credentials);
      }),
      tap(response => {
        // Nach erfolgreichem Login werden neue CSRF-Tokens gesetzt
        console.log('Login successful, new CSRF tokens issued');
      })
    );
  }
  
  /**
   * CSRF-Token vor dem ersten API-Call abrufen
   */
  obtainCsrfToken(): Observable<void> {
    // Dieser Call setzt das CSRF-Cookie für nachfolgende Requests
    return this.http.get<void>(`${this.API_URL}/csrf-token`);
  }
  
  /**
   * Logout mit CSRF-Schutz
   */
  logout(): Observable<void> {
    // Logout ist ein state-changing Request und benötigt CSRF-Schutz
    return this.http.post<void>(`${this.API_URL}/logout`, {}).pipe(
      tap(() => {
        // CSRF-Token nach Logout löschen
        this.clearCsrfToken();
      })
    );
  }
  
  private clearCsrfToken(): void {
    // CSRF-Cookie löschen (falls möglich über JavaScript)
    document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
  }
}

22.3.5 CSRF-bewusste Interceptors

import { Injectable, inject } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Prüfen, ob es sich um einen state-changing Request handelt
    if (this.isStateChangingMethod(req.method)) {
      
      // Für externe APIs CSRF-Header entfernen (Sicherheit)
      if (this.isExternalUrl(req.url)) {
        const modifiedReq = req.clone({
          setHeaders: {
            // CSRF-Header für externe URLs entfernen
            'X-XSRF-TOKEN': ''
          }
        });
        return next.handle(modifiedReq);
      }
      
      // Für interne APIs: CSRF-Token validieren
      const csrfToken = this.getCsrfToken();
      if (!csrfToken && this.requiresCsrfToken(req.url)) {
        console.warn('CSRF token missing for state-changing request to:', req.url);
      }
    }
    
    return next.handle(req);
  }
  
  private isStateChangingMethod(method: string): boolean {
    return ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method.toUpperCase());
  }
  
  private isExternalUrl(url: string): boolean {
    return url.startsWith('http') && !url.includes(window.location.hostname);
  }
  
  private requiresCsrfToken(url: string): boolean {
    // URLs die CSRF-Schutz benötigen
    const protectedEndpoints = ['/api/users', '/api/orders', '/api/payments'];
    return protectedEndpoints.some(endpoint => url.includes(endpoint));
  }
  
  private getCsrfToken(): string | null {
    const cookies = document.cookie.split(';');
    const xsrfCookie = cookies.find(cookie => 
      cookie.trim().startsWith('XSRF-TOKEN=')
    );
    return xsrfCookie ? decodeURIComponent(xsrfCookie.split('=')[1]) : null;
  }
}

22.3.6 CSRF-Schutz für File Uploads

File-Uploads erfordern besondere Aufmerksamkeit beim CSRF-Schutz:

import { Component, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-secure-file-upload',
  template: `
    <form (ngSubmit)="uploadFile()">
      <input type="file" (change)="onFileSelected($event)" #fileInput>
      <button type="submit" [disabled]="!selectedFile">Upload</button>
    </form>
    
    @if (uploadProgress > 0) {
      <div class="progress">
        <div class="progress-bar" [style.width.%]="uploadProgress"></div>
      </div>
    }
  `
})
export class SecureFileUploadComponent {
  private http = inject(HttpClient);
  
  selectedFile: File | null = null;
  uploadProgress = 0;
  
  onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    if (input.files?.length) {
      this.selectedFile = input.files[0];
    }
  }
  
  uploadFile(): void {
    if (!this.selectedFile) return;
    
    const formData = new FormData();
    formData.append('file', this.selectedFile);
    
    // CSRF-Token manuell zu FormData hinzufügen
    const csrfToken = this.getCsrfToken();
    if (csrfToken) {
      formData.append('_token', csrfToken);
    }
    
    // Alternative: CSRF-Token im Header (bevorzugt)
    const headers = new HttpHeaders({
      'X-XSRF-TOKEN': csrfToken || ''
    });
    
    this.http.post('/api/upload', formData, {
      headers,
      reportProgress: true,
      observe: 'events'
    }).subscribe({
      next: (event) => {
        if (event.type === HttpEventType.UploadProgress) {
          this.uploadProgress = Math.round(100 * event.loaded / (event.total || 1));
        }
      },
      error: (error) => {
        console.error('Upload failed:', error);
        if (error.status === 403) {
          console.error('CSRF token validation failed');
        }
      }
    });
  }
  
  private getCsrfToken(): string | null {
    const cookies = document.cookie.split(';');
    const xsrfCookie = cookies.find(cookie => 
      cookie.trim().startsWith('XSRF-TOKEN=')
    );
    return xsrfCookie ? decodeURIComponent(xsrfCookie.split('=')[1]) : null;
  }
}

22.3.7 CSRF-Schutz mit Angular Forms

import { Component, inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-secure-form',
  template: `
    <form [formGroup]="secureForm" (ngSubmit)="onSubmit()">
      <!-- Verstecktes CSRF-Token-Feld für zusätzliche Sicherheit -->
      <input type="hidden" [value]="csrfToken" formControlName="csrfToken">
      
      <div class="form-group">
        <label for="email">E-Mail</label>
        <input type="email" id="email" formControlName="email" class="form-control">
      </div>
      
      <div class="form-group">
        <label for="message">Nachricht</label>
        <textarea id="message" formControlName="message" class="form-control"></textarea>
      </div>
      
      <button type="submit" [disabled]="secureForm.invalid || isSubmitting">
        Senden
      </button>
    </form>
  `
})
export class SecureFormComponent implements OnInit {
  private fb = inject(FormBuilder);
  private http = inject(HttpClient);
  
  secureForm!: FormGroup;
  csrfToken = '';
  isSubmitting = false;
  
  ngOnInit(): void {
    this.csrfToken = this.getCsrfToken() || '';
    
    this.secureForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      message: ['', Validators.required],
      csrfToken: [this.csrfToken] // CSRF-Token als verstecktes Feld
    });
  }
  
  onSubmit(): void {
    if (this.secureForm.invalid) return;
    
    this.isSubmitting = true;
    
    // Form-Daten enthalten automatisch das CSRF-Token
    const formData = this.secureForm.value;
    
    this.http.post('/api/contact', formData).subscribe({
      next: (response) => {
        console.log('Form submitted successfully');
        this.secureForm.reset();
        this.isSubmitting = false;
      },
      error: (error) => {
        if (error.status === 403) {
          console.error('CSRF validation failed - refreshing token');
          this.refreshCsrfToken();
        }
        this.isSubmitting = false;
      }
    });
  }
  
  private getCsrfToken(): string | null {
    const cookies = document.cookie.split(';');
    const xsrfCookie = cookies.find(cookie => 
      cookie.trim().startsWith('XSRF-TOKEN=')
    );
    return xsrfCookie ? decodeURIComponent(xsrfCookie.split('=')[1]) : null;
  }
  
  private refreshCsrfToken(): void {
    // CSRF-Token neu abrufen nach Validierungsfehler
    this.http.get('/api/csrf-token').subscribe(() => {
      this.csrfToken = this.getCsrfToken() || '';
      this.secureForm.patchValue({ csrfToken: this.csrfToken });
    });
  }
}

22.3.8 Testing von CSRF-Schutz

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { CsrfAwareApiService } from './csrf-aware-api.service';

describe('CsrfAwareApiService', () => {
  let service: CsrfAwareApiService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [CsrfAwareApiService]
    });
    
    service = TestBed.inject(CsrfAwareApiService);
    httpMock = TestBed.inject(HttpTestingController);
    
    // CSRF-Cookie für Tests setzen
    document.cookie = 'XSRF-TOKEN=test-csrf-token; path=/';
  });
  
  afterEach(() => {
    httpMock.verify();
    // Cookie nach Test löschen
    document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
  });
  
  it('should include CSRF token in POST requests', () => {
    const userData = { name: 'Test User', email: 'test@example.com' };
    
    service.createUser(userData).subscribe();
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('POST');
    expect(req.request.headers.get('X-XSRF-TOKEN')).toBe('test-csrf-token');
    
    req.flush({ id: 1, ...userData });
  });
  
  it('should handle missing CSRF token gracefully', () => {
    // CSRF-Cookie entfernen
    document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    
    const userData = { name: 'Test User' };
    
    service.createUser(userData).subscribe();
    
    const req = httpMock.expectOne('/api/users');
    // Angular sollte trotzdem den Request senden, aber ohne CSRF-Token
    expect(req.request.headers.has('X-XSRF-TOKEN')).toBeFalsy();
  });
});

22.3.9 Best Practices für CSRF-Schutz in Angular

  1. Verwenden Sie Angular’s eingebauten CSRF-Schutz: Nutzen Sie withXsrfConfiguration() für automatischen Schutz
  2. Sichere Cookie-Konfiguration: Stellen Sie sicher, dass CSRF-Cookies SameSite=Strict oder SameSite=Lax verwenden
  3. Validierung auf dem Server: CSRF-Schutz muss immer serverseitig validiert werden
  4. HTTPS verwenden: CSRF-Tokens sollten nur über sichere Verbindungen übertragen werden
  5. Token-Rotation: Implementieren Sie regelmäßige CSRF-Token-Erneuerung
  6. Externe URLs: Entfernen Sie CSRF-Header bei Requests an externe APIs

22.4 Sicherheitshinweise für moderne Angular-Anwendungen

  1. Verwenden Sie secure HttpOnly Cookies für besonders sensible Anwendungen
  2. Implementieren Sie Cross-Site-Request-Forgery (CSRF) Schutz
  3. Überwachen Sie Inaktivität und implementieren Sie Auto-Logout
  4. Behandeln Sie sensible Daten sicher
  5. Implementieren Sie Content Security Policy (CSP)
  6. Setzen Sie auf Progressive Enhancement für WebAuthn/FIDO2
  7. Verbessern Sie die UX mit adaptiven Authentifizierungsmethoden