Introduction
Welcome back, future security champions! In our previous chapters, we’ve explored the foundational principles of web security, delved into the attacker’s mindset, and dissected the notorious OWASP Top 10 vulnerabilities. We’ve even touched upon secure coding practices for modern frontend frameworks. Now, it’s time to put all that knowledge into action!
In this chapter, we’re going to tackle a common real-world scenario: securing an existing Angular dashboard application. Imagine you’ve inherited a functional dashboard that displays user-specific data, but it wasn’t built with security as a top priority. Your mission, should you choose to accept it, is to fortify this application against common threats. We’ll focus on implementing robust authentication, protecting against Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF), and ensuring secure communication with our backend API.
This hands-on project will solidify your understanding of how security concepts translate into practical code. By the end, you’ll have a much clearer picture of how to proactively build and retrofit security into your Angular applications, making them resilient against attacks.
Prerequisites: To get the most out of this chapter, you should have a basic understanding of Angular (components, services, routing), a grasp of HTTP requests, and familiarity with the core web security concepts and OWASP Top 10 vulnerabilities covered in previous chapters.
Core Concepts: Laying the Secure Foundation
Before we jump into code, let’s briefly revisit the key security concepts we’ll be applying to our Angular dashboard.
The “Existing” Angular Dashboard Structure
For this exercise, we’ll simulate a typical Angular dashboard application. It will likely have:
- A Login Component: Where users enter credentials.
- An Authentication Service: To handle login, logout, and token management.
- Dashboard Components: Displaying various pieces of information (e.g., user profiles, data tables).
- Data Services: To fetch data from a backend API.
- Routing: To navigate between the login and dashboard areas.
Our goal isn’t to build this app from scratch, but rather to identify common areas where vulnerabilities exist and systematically inject security best practices.
Authentication and Authorization in Angular
Authentication is about verifying who a user is, while authorization is about what that user is allowed to do. For modern web applications, token-based authentication (like JSON Web Tokens or JWTs) is prevalent.
Secure Token Handling (2026 Best Practices)
One of the most critical aspects of frontend security is how you handle authentication tokens.
- Access Tokens (JWTs): These are short-lived tokens used to authenticate API requests. For maximum security, they should ideally be stored in memory (e.g., in a private variable within an authentication service). Why? Because
localStorageorsessionStorageare vulnerable to XSS attacks. If an attacker injects malicious JavaScript, they can easily steal tokens from these storages. Storing in memory makes this harder, as the token only exists while the script is running. - Refresh Tokens: These are longer-lived tokens used to obtain new access tokens without requiring the user to re-authenticate. Refresh tokens are highly sensitive and should never be stored in
localStorageorsessionStorage. The gold standard is to store them in HttpOnly, Secure, SameSite=Strict cookies.- HttpOnly: Makes the cookie inaccessible to client-side JavaScript, mitigating XSS risks.
- Secure: Ensures the cookie is only sent over HTTPS connections.
- SameSite=Strict: Prevents the browser from sending the cookie with cross-site requests, offering robust CSRF protection.
Angular Guards and Interceptors
Angular provides powerful tools to manage authentication and authorization flows:
- Auth Guards (
CanActivate): These are services that implement theCanActivateinterface. They determine if a user can access a route. If the user isn’t authenticated, the guard can redirect them to the login page. - HTTP Interceptors: These allow you to intercept incoming and outgoing HTTP requests. We’ll use an interceptor to automatically attach our access token to every outgoing API request, streamlining our authentication process.
Preventing Cross-Site Scripting (XSS) in Angular
XSS attacks occur when an attacker injects malicious client-side scripts into web pages viewed by other users. Angular has robust built-in protections against XSS.
- Automatic Sanitization: By default, Angular treats all values as untrusted and sanitizes them before inserting them into the DOM. This means if you try to bind HTML content directly, Angular will strip out potentially dangerous elements like
<script>tags. DomSanitizer: Sometimes, you need to render dynamic HTML (e.g., from a rich text editor). In such cases, you must explicitly tell Angular that the content is safe usingDomSanitizer. This is a powerful tool, but use it with extreme caution and only for content you have thoroughly validated and trusted.
Protecting Against Cross-Site Request Forgery (CSRF)
CSRF attacks trick authenticated users into executing unwanted actions on a web application. The attacker crafts a malicious request (e.g., transferring funds, changing a password) and embeds it on a site controlled by them. When the victim visits the attacker’s site, their browser automatically includes their session cookies for the target site, executing the unwanted action.
- Synchronizer Token Pattern: This is a common and effective defense. The server generates a unique, unpredictable token, sends it to the client (e.g., in an
HttpOnlycookie), and the client includes it in a custom HTTP header (e.g.,X-XSRF-TOKEN) with every state-changing request (POST, PUT, DELETE). The server then verifies that the token in the header matches the one in the cookie. - Angular’s
HttpClientXsrfModule: Angular provides built-in support for the synchronizer token pattern, making it relatively easy to implement on the frontend. It automatically reads a configured cookie (usuallyXSRF-TOKEN) and adds it as anX-XSRF-TOKENheader to all non-GET requests. - SameSite Cookies: As mentioned, using
SameSite=StrictorLaxfor session cookies (including refresh tokens) is a powerful defense against CSRF, as it prevents the browser from sending cookies with cross-site requests.
Secure API Communication
- HTTPS Everywhere: Always use HTTPS to encrypt all communication between your Angular frontend and your backend API. This prevents eavesdropping and tampering.
- CORS (Cross-Origin Resource Sharing): Properly configure CORS on your backend to specify which origins (your Angular app’s domain) are allowed to make requests. A misconfigured CORS policy can lead to serious security vulnerabilities.
- Input Validation: While backend validation is paramount, client-side validation in Angular provides immediate feedback to users and reduces unnecessary server load. However, remember that client-side validation is never a substitute for robust backend validation, as attackers can bypass frontend checks.
Step-by-Step Implementation
Let’s get our hands dirty! We’ll start by setting up a minimal Angular application that mimics our “existing” dashboard, and then incrementally add security features.
Project Setup (Angular v17.x.x)
First, ensure you have the Angular CLI installed. As of January 2026, Angular CLI v17.x.x is widely used, often leveraging standalone components by default. We’ll use this modern approach.
Create a New Angular Project: Open your terminal and run:
ng new secure-angular-dashboard --standalone true --routing true --style css cd secure-angular-dashboardThis creates a new project with standalone components and routing enabled.
Simulate a Simple Dashboard Structure: We’ll create a basic login component and a dashboard component.
ng generate component auth/login --skip-tests ng generate component dashboard/home --skip-tests ng generate service services/auth --skip-tests ng generate service services/data --skip-testsConfigure Basic Routing: Open
src/app/app.routes.tsand set up routes:// src/app/app.routes.ts import { Routes } from '@angular/router'; import { LoginComponent } from './auth/login/login.component'; import { HomeComponent } from './dashboard/home/home.component'; export const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'dashboard', component: HomeComponent }, { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: '**', redirectTo: '/dashboard' } // Wildcard for unknown routes ];Now, open
src/app/app.component.tsand ensureRouterOutletis imported and used:// src/app/app.component.ts import { Component } from '@angular/core'; import { RouterOutlet, RouterLink } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, RouterLink], // Add RouterLink for navigation template: ` <header> <h1>Secure Angular Dashboard</h1> <nav> <a routerLink="/dashboard">Dashboard</a> | <a routerLink="/login">Login</a> <button (click)="logout()">Logout</button> </nav> </header> <main> <router-outlet></router-outlet> </main> `, styleUrl: './app.component.css' }) export class AppComponent { title = 'secure-angular-dashboard'; logout() { // This will be implemented later console.log('Logout clicked'); } }You can run
ng serveand navigate to/loginor/dashboardto see the basic structure.
Step 1: Implementing Basic Authentication Flow
We’ll create a mock authentication service and use an Auth Guard to protect the dashboard route.
Update
AuthService: This service will simulate login/logout and hold our (in-memory) access token.// src/app/services/auth.service.ts import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Observable, of, throwError } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AuthService { private accessToken: string | null = null; // Stored in memory constructor(private router: Router) { } // Simulate a login request login(username: string, password: string): Observable<boolean> { // In a real app, this would be an HTTP POST request to your backend // If credentials are valid, backend returns JWT access token and refresh token (in HttpOnly cookie) if (username === 'user' && password === 'password') { const mockAccessToken = 'mock_jwt_access_token_12345'; this.accessToken = mockAccessToken; console.log('Logged in successfully, token stored in memory.'); return of(true).pipe( tap(() => this.router.navigate(['/dashboard'])) ); } else { return throwError(() => new Error('Invalid credentials')); } } logout(): void { this.accessToken = null; // Clear token from memory // In a real app, you might also want to call a backend endpoint to invalidate the refresh token console.log('Logged out, token cleared.'); this.router.navigate(['/login']); } isAuthenticated(): boolean { return !!this.accessToken; // Check if an access token exists } getAccessToken(): string | null { return this.accessToken; } }Update
LoginComponent: Bind the login form to theAuthService.// src/app/auth/login/login.component.ts import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; // Import FormsModule import { AuthService } from '../../services/auth.service'; import { CommonModule } from '@angular/common'; // For ngIf @Component({ selector: 'app-login', standalone: true, imports: [FormsModule, CommonModule], // Add FormsModule and CommonModule template: ` <h2>Login</h2> <form (ngSubmit)="onLogin()"> <p *ngIf="errorMessage" class="error">{{ errorMessage }}</p> <div> <label for="username">Username:</label> <input id="username" type="text" [(ngModel)]="username" name="username" required> </div> <div> <label for="password">Password:</label> <input id="password" type="password" [(ngModel)]="password" name="password" required> </div> <button type="submit">Login</button> </form> `, styles: [` .error { color: red; } div { margin-bottom: 10px; } label { display: block; } `] }) export class LoginComponent { username = ''; password = ''; errorMessage: string | null = null; constructor(private authService: AuthService) { } onLogin(): void { this.errorMessage = null; this.authService.login(this.username, this.password).subscribe({ next: () => { console.log('Login successful!'); }, error: (err: Error) => { this.errorMessage = err.message; console.error('Login failed:', err.message); } }); } }Create an Auth Guard: This guard will protect our dashboard routes.
// src/app/guards/auth.guard.ts import { CanActivateFn, Router } from '@angular/router'; import { inject } from '@angular/core'; import { AuthService } from '../services/auth.service'; export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); if (authService.isAuthenticated()) { return true; } else { router.navigate(['/login']); return false; } };Apply the Auth Guard to Routes: Update
src/app/app.routes.tsto use the guard.// src/app/app.routes.ts import { Routes } from '@angular/router'; import { LoginComponent } from './auth/login/login.component'; import { HomeComponent } from './dashboard/home/home.component'; import { authGuard } from './guards/auth.guard'; // Import the guard export const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'dashboard', component: HomeComponent, canActivate: [authGuard] // Apply the guard here! }, { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: '**', redirectTo: '/dashboard' } ];Now, if you try to go to
/dashboardwithout logging in, you’ll be redirected to/login.Add Logout to
AppComponent: Connect the logout button.// src/app/app.component.ts (partial) import { AuthService } from './services/auth.service'; // Import AuthService // ... inside AppComponent class export class AppComponent { title = 'secure-angular-dashboard'; constructor(private authService: AuthService) { } // Inject AuthService logout() { this.authService.logout(); } }
Step 2: Implementing an HTTP Interceptor for Tokens
Now, let’s automatically attach our access token to outgoing API requests.
Create Token Interceptor:
ng generate interceptor interceptors/token --skip-tests// src/app/interceptors/token.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; import { AuthService } from '../services/auth.service'; export const tokenInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const accessToken = authService.getAccessToken(); if (accessToken) { // Clone the request and add the authorization header const clonedReq = req.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } }); console.log('TokenInterceptor: Token attached to request.'); return next(clonedReq); } console.log('TokenInterceptor: No token to attach.'); return next(req); };Register the Interceptor: In a standalone Angular application, interceptors are registered in
app.config.ts.// src/app/app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; // Import provideHttpClient and withInterceptors import { routes } from './app.routes'; import { tokenInterceptor } from './interceptors/token.interceptor'; // Import your interceptor export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient(withInterceptors([tokenInterceptor])) // Register the interceptor ] };Test with a Mock Data Service: Let’s make a dummy API call from the
HomeComponentto see the interceptor in action.// src/app/services/data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { delay } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class DataService { // In a real app, this would be your backend API URL private apiUrl = 'https://api.example.com/data'; // Dummy URL constructor(private http: HttpClient) { } getDashboardData(): Observable<any[]> { console.log('DataService: Fetching dashboard data...'); // Simulate an HTTP GET request that requires authentication // The token interceptor will add the Authorization header // For this demo, we'll return mock data directly return of([ { id: 1, name: 'Item A', value: 100 }, { id: 2, name: 'Item B', value: 200 }, { id: 3, name: 'Item C', value: 150 } ]).pipe(delay(500)); // Simulate network delay } }// src/app/dashboard/home/home.component.ts import { Component, OnInit } from '@angular/core'; import { DataService } from '../../services/data.service'; import { CommonModule } from '@angular/common'; // For ngFor @Component({ selector: 'app-home', standalone: true, imports: [CommonModule], template: ` <h2>Dashboard Home</h2> <p>Welcome to your secure dashboard!</p> <button (click)="fetchData()">Fetch Protected Data</button> <div *ngIf="data"> <h3>Protected Data:</h3> <ul> <li *ngFor="let item of data">ID: {{ item.id }}, Name: {{ item.name }}, Value: {{ item.value }}</li> </ul> </div> <p *ngIf="errorMessage" class="error">{{ errorMessage }}</p> `, styles: [` .error { color: red; } `] }) export class HomeComponent implements OnInit { data: any[] | null = null; errorMessage: string | null = null; constructor(private dataService: DataService) { } ngOnInit(): void { this.fetchData(); // Fetch data on component load } fetchData(): void { this.errorMessage = null; this.dataService.getDashboardData().subscribe({ next: (res) => { this.data = res; console.log('Dashboard data fetched:', res); }, error: (err) => { this.errorMessage = 'Failed to fetch data. Are you logged in?'; console.error('Error fetching dashboard data:', err); } }); } }Now, log in as
user/password. Navigate to the dashboard. Check your browser’s developer console network tab. You should see a request tohttps://api.example.com/data(even if it fails, it’s just a mock URL) with anAuthorization: Bearer mock_jwt_access_token_12345header. Success!
Step 3: XSS Prevention with Angular’s Sanitization
Angular provides excellent built-in XSS protection. Let’s demonstrate it and then the careful use of DomSanitizer.
Demonstrate Automatic Sanitization: Add a potentially malicious string to
HomeComponent.// src/app/dashboard/home/home.component.ts (partial) import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; // Import DomSanitizer and SafeHtml // ... inside HomeComponent class export class HomeComponent implements OnInit { // ... existing properties // Malicious string maliciousHtml = '<script>alert("XSS Attack!")</script><p>Malicious content</p>'; sanitizedHtml: SafeHtml; // Property to hold sanitized HTML constructor(private dataService: DataService, private sanitizer: DomSanitizer) { } // Inject DomSanitizer ngOnInit(): void { this.fetchData(); // Sanitize the malicious HTML this.sanitizedHtml = this.sanitizer.bypassSecurityTrustHtml(this.maliciousHtml); // DANGER: This is for demo! } // ... existing methods }Now, update the
home.component.tstemplate to display both unsanitized and explicitly sanitized content.<!-- src/app/dashboard/home/home.component.ts (template partial) --> <!-- ... existing template content --> <h3>XSS Demonstration:</h3> <p>Angular's default binding:</p> <div [innerHTML]="maliciousHtml"></div> <!-- Angular will sanitize this automatically --> <p>Using DomSanitizer (DANGER! ONLY for TRUSTED content):</p> <div [innerHTML]="sanitizedHtml"></div> <!-- This will execute the script if not careful -->When you run this:
- The first
divusing[innerHTML]="maliciousHtml"will likely show “Malicious content” but the<script>tag will be stripped, and no alert will fire. Angular’s built-in sanitizer protects you. - The second
divusing[innerHTML]="sanitizedHtml"(where we explicitly bypassed security withbypassSecurityTrustHtml) WILL trigger thealert("XSS Attack!"). This demonstrates the power and danger ofDomSanitizer. Only usebypassSecurityTrustHtmlwhen you are absolutely certain the content originates from a trusted source and has been thoroughly validated on the backend!
- The first
Step 4: CSRF Protection with HttpClientXsrfModule
Angular’s HttpClientXsrfModule provides an easy way to integrate CSRF protection on the frontend. Remember, this requires backend cooperation to set the XSRF-TOKEN cookie.
Configure
HttpClientXsrfModule: Insrc/app/app.config.ts, add the CSRF provider.// src/app/app.config.ts (partial) import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http'; // Import withXsrfConfiguration import { routes } from './app.routes'; import { tokenInterceptor } from './interceptors/token.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient( withInterceptors([tokenInterceptor]), withXsrfConfiguration({ cookieName: 'XSRF-TOKEN', // The name of the cookie your backend sets headerName: 'X-XSRF-TOKEN' // The name of the header Angular will send }) ) ] };With this configuration, Angular’s
HttpClientwill automatically look for a cookie namedXSRF-TOKENand, if found, include its value in anX-XSRF-TOKENheader for all non-GET, non-HEAD requests.Backend Requirement: Your backend API must set an
HttpOnly,Secure,SameSite=LaxorSameSite=Strictcookie namedXSRF-TOKEN(or whatevercookieNameyou configure) on the initial load or login. The value of this cookie should be a cryptographically secure random string. The backend then verifies this header on subsequent state-changing requests.
Step 5: Secure Storage for Tokens (Refinement)
While we used in-memory storage for the access token, let’s explicitly discuss and refine the concept of refresh tokens and their secure storage.
Conceptual Implementation (No Code Change Needed for this demo):
In a real application:
- Login: When a user logs in, your backend would send:
- An
accessToken(JWT) in the HTTP response body. - A
refreshTokenin anHttpOnly,Secure,SameSite=Strictcookie.
- An
- Auth Service:
- The
AuthServicewould store theaccessTokenin an in-memory variable. - It would not directly access the
refreshTokencookie (due to HttpOnly).
- The
- Token Refresh Interceptor (Advanced): You would typically have another HTTP interceptor. If an API call returns a
401 Unauthorizederror (due to an expired access token), this interceptor would:- Pause the original request.
- Make a separate request to a
/refresh-tokenendpoint on your backend. This request would automatically send theHttpOnlyrefresh token cookie. - If successful, the backend returns a new
accessToken(and potentially a new refresh token cookie). - The interceptor updates the
accessTokeninAuthServiceand retries the original failed request with the new token. - If refresh fails, the user is logged out.
This pattern ensures that the sensitive refresh token is never exposed to JavaScript, significantly reducing the risk of session hijacking via XSS.
Mini-Challenge: Implement a “Remember Me” Feature (Securely!)
Challenge: Extend our authentication flow to include a “Remember Me” option. If a user checks “Remember Me” during login, their session should persist longer (e.g., beyond browser tab closure) without storing sensitive tokens in localStorage.
Hint: Think about what kind of token enables long-lived sessions and how that token should be securely stored. Focus on the conceptual frontend implementation, assuming a backend that supports longer-lived refresh tokens when “Remember Me” is active.
What to Observe/Learn: How to manage different session durations, the critical distinction between client-side and server-side session persistence, and why localStorage is not the answer for sensitive data.
Solution Approach (Do NOT copy-paste, try to implement first!):
- Login Component: Add a checkbox for “Remember Me” to the login form.
- Auth Service: Modify the
loginmethod to accept arememberMeboolean.- If
rememberMeis true, your simulated backend response (or the actual backend if you were building one) would indicate that a longer-lived refresh token has been set in anHttpOnly,Secure,SameSite=Strictcookie. - If
rememberMeis false, a standard, shorter-lived refresh token would be used.
- If
- No
localStorage! The key here is that the frontend does not store the refresh token itself. It relies on the browser’s cookie mechanism and the backend’s session management. The “Remember Me” option simply tells the backend to issue a refresh token with a longer expiry.
Common Pitfalls & Troubleshooting
Even with Angular’s help, security can be tricky. Here are some common mistakes:
- Storing Tokens in
localStorageorsessionStorage: This is the most common and dangerous pitfall. While convenient, it makes your application highly vulnerable to XSS attacks, as any injected script can easily read these storages. Always prefer in-memory for access tokens and HttpOnly cookies for refresh tokens. - Ignoring Backend Security: A secure frontend is only one layer. If your backend API has SQL injection, broken access control, or other vulnerabilities, your frontend efforts will be undermined. Security is a full-stack responsibility!
- Misusing
DomSanitizer: UsingbypassSecurityTrustHtml(or otherbypassSecurityTrust*methods) without thoroughly validating and sanitizing the input on the backend is an open invitation for XSS. Only use it when rendering truly trusted content. - Incomplete CSRF Protection: Relying solely on
SameSitecookies without a synchronizer token for critical actions might still leave some edge cases open, especially with older browser versions or specific attack vectors. A layered approach is best. - CORS Misconfiguration: An overly permissive CORS policy (e.g., allowing
*forAccess-Control-Allow-Originin production) can allow malicious sites to interact with your API, leading to data leaks or unwanted actions. - Reliance on Frontend for Authorization: Hiding buttons or routes based on user roles in the frontend is good for UX, but it’s never a security measure. Attackers can easily bypass frontend checks. All authorization decisions must be enforced on the backend.
Summary
Phew! You’ve just taken a significant step in transforming a potentially vulnerable Angular dashboard into a more secure application. Let’s recap the key takeaways:
- Authentication: Implement token-based authentication (JWTs). Store access tokens in-memory and sensitive refresh tokens in
HttpOnly,Secure,SameSite=Strictcookies. - Auth Guards: Use Angular’s
CanActivateguards to protect routes based on authentication status. - HTTP Interceptors: Leverage interceptors to automatically attach authentication tokens to API requests.
- XSS Prevention: Trust Angular’s built-in sanitization. Only use
DomSanitizer’sbypassSecurityTrust*methods with extreme caution and for truly trusted, validated content. - CSRF Protection: Utilize Angular’s
HttpClientXsrfModulein conjunction with a backend that setsHttpOnly,Secure,SameSitecookies and validates theX-XSRF-TOKENheader. - Secure Communication: Always enforce HTTPS for all API interactions.
- Layered Security: Remember that frontend security is just one layer. It must be complemented by robust backend security measures.
By applying these principles, you’re not just building features; you’re building trust and resilience into your applications. Keep practicing, keep learning, and keep asking “how could this be exploited?"—that’s the attacker’s mindset you need to cultivate!
What’s Next? In the next chapter, we’ll shift our focus to securing backend APIs, which is the other critical half of building secure web applications.
References
- OWASP Top 10 (2021) - Official Documentation
- Angular HttpClient Guide - Interceptors
- Angular Router - Guards
- Angular Security Documentation
- MDN Web Docs - HttpOnly Cookies
- MDN Web Docs - SameSite Cookies
- OWASP Cheat Sheet Series - Cross-Site Request Forgery Prevention
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.