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.
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;
}
}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!)))
);
}
}
}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;
})
);
};// 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' }
}
];// 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);
}
}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.
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
})
});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));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;
}
}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=/;';
}
}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;
}
}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;
}
}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 });
});
}
}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();
});
});withXsrfConfiguration() für automatischen
SchutzSameSite=Strict oder
SameSite=Lax verwenden