Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Angular Security

Module Overview

Estimated Time: 3-4 hours | Difficulty: Advanced | Prerequisites: Module 13
Security is critical for any web application. Angular provides built-in protections, but developers must understand and properly implement security measures. Think of security like a medieval castle’s defenses. Angular gives you thick stone walls (automatic XSS sanitization) and a moat (template injection prevention) for free. But you still need to staff the gatehouse (authentication), post guards on the watchtowers (authorization), and maintain the walls (dependency updates). The framework handles the most common attacks automatically, but the attacks it cannot anticipate — CSRF, misconfigured auth tokens, overly permissive CORS — are your responsibility.
The most dangerous misconception in frontend security: “The API will handle it.” Client-side security is a UX convenience layer — it keeps honest users from accidentally doing wrong things. A malicious user can open DevTools and bypass every client-side check. Always validate, authorize, and sanitize on the server. Every single time. No exceptions.
What You’ll Learn:
  • XSS protection
  • CSRF/XSRF protection
  • Content Security Policy
  • Authentication patterns
  • Secure HTTP practices
  • Input validation

Angular Security Model

┌─────────────────────────────────────────────────────────────────────────┐
│              Angular Built-in Security                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Automatic Protections:                                                 │
│   ─────────────────────                                                  │
│   ✓ XSS: All values sanitized by default                               │
│   ✓ Template injection: No dynamic template generation                  │
│   ✓ URL sanitization: Unsafe URLs blocked                               │
│   ✓ Style sanitization: Inline styles checked                           │
│                                                                          │
│   Developer Responsibilities:                                            │
│   ───────────────────────────                                            │
│   • CSRF token handling                                                  │
│   • Content Security Policy headers                                      │
│   • Authentication/Authorization                                         │
│   • Input validation                                                     │
│   • Secure API communication                                             │
│   • Dependency security updates                                          │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

XSS Protection

Angular automatically escapes all values bound to the DOM. This is not optional — it happens on every binding, every render cycle. You cannot accidentally disable it. This single feature prevents the vast majority of XSS attacks that plague vanilla JavaScript and jQuery applications.

Automatic Sanitization

@Component({
  template: `
    <!-- SAFE: Automatically escaped by Angular's DomSanitizer -->
    <p>{{ userInput }}</p>
    <div [innerHTML]="htmlContent"></div>
    <a [href]="userUrl">Link</a>
    <img [src]="imageUrl" />
  `
})
export class SafeComponent {
  // Even malicious input is escaped -- Angular converts this to literal
  // text, so the browser renders "<script>alert("XSS")</script>" as
  // visible characters, not as executable code.
  userInput = '<script>alert("XSS")</script>';  // Rendered as text

  // HTML is sanitized: safe tags like <p> are kept, but <script> tags
  // and event handlers (onclick, onerror, etc.) are stripped out entirely.
  htmlContent = '<p>Safe</p><script>alert("XSS")</script>';  // Script removed

  // Dangerous URL schemes are blocked. Angular recognizes javascript:,
  // data:, and other unsafe protocols and replaces them with "unsafe:..."
  userUrl = 'javascript:alert("XSS")';  // Blocked, shows "unsafe:..."
}

Bypassing Sanitization (Use with Caution!)

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  template: `<div [innerHTML]="trustedHtml"></div>`
})
export class UnsafeComponent {
  private sanitizer = inject(DomSanitizer);
  trustedHtml: SafeHtml;
  
  constructor() {
    // ⚠️ DANGER: Only use for truly trusted content
    // Verify the source is secure before bypassing!
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(
      '<p onclick="safe()">Trusted content from server</p>'
    );
  }
}

// Available bypass methods:
// - bypassSecurityTrustHtml(value)
// - bypassSecurityTrustStyle(value)
// - bypassSecurityTrustScript(value)
// - bypassSecurityTrustUrl(value)
// - bypassSecurityTrustResourceUrl(value)
Never bypass sanitization for user input! Only use bypassSecurityTrust* for content from trusted sources that you fully control — such as HTML generated by your own CMS admin interface. If you use it on user-submitted content (comments, profile bios, form inputs), you are handing the user a direct line to execute arbitrary JavaScript in every visitor’s browser. This is how account hijacking, session theft, and data exfiltration happen.

Safe HTML Pipe

// safe-html.pipe.ts
@Pipe({ name: 'safeHtml', standalone: true })
export class SafeHtmlPipe implements PipeTransform {
  private sanitizer = inject(DomSanitizer);
  
  transform(value: string): SafeHtml {
    // Still sanitized, but allows safe HTML tags
    return this.sanitizer.bypassSecurityTrustHtml(value);
  }
}

// Better: Allowlist specific tags/attributes
@Pipe({ name: 'sanitizeHtml', standalone: true })
export class SanitizeHtmlPipe implements PipeTransform {
  private allowedTags = ['p', 'br', 'strong', 'em', 'a', 'ul', 'li'];
  private allowedAttrs = ['href', 'target'];
  
  transform(value: string): string {
    // Use DOMPurify or similar library
    return DOMPurify.sanitize(value, {
      ALLOWED_TAGS: this.allowedTags,
      ALLOWED_ATTR: this.allowedAttrs
    });
  }
}

CSRF/XSRF Protection

CSRF (Cross-Site Request Forgery) is an attack where a malicious website tricks the user’s browser into making authenticated requests to your API. For example, if a user is logged into their banking app and visits a malicious page, that page could submit a hidden form to bank.com/transfer?to=attacker&amount=10000. The browser automatically attaches the bank’s session cookies, so the request looks legitimate. The defense: the server generates a unique token and sends it as a cookie. Angular’s HttpClient reads the cookie and sends the token back as a header. Since the malicious site cannot read cross-origin cookies, it cannot forge the header.

HttpClient XSRF Support

// app.config.ts
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',     // Angular reads the token from this cookie
        headerName: 'X-XSRF-TOKEN'    // Angular sends it back in this header
        // The server compares cookie vs header -- if they match, the request
        // is legitimate. An attacker's page cannot read the cookie to forge it.
      })
    )
  ]
};
// Server-side: Set XSRF cookie
// Express example
app.use((req, res, next) => {
  res.cookie('XSRF-TOKEN', generateCsrfToken(), {
    httpOnly: false,  // Must be readable by JavaScript
    secure: true,
    sameSite: 'strict'
  });
  next();
});

// Server-side: Verify token
app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const token = req.headers['x-xsrf-token'];
    if (!verifyCsrfToken(token)) {
      return res.status(403).json({ error: 'Invalid CSRF token' });
    }
  }
  next();
});

Content Security Policy

Configure CSP Headers

// server.ts (Express)
import helmet from 'helmet';

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],  // Angular needs inline for styles
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.myapp.com"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

Nonce-based CSP (More Secure)

// Generate nonce per request
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(helmet.contentSecurityPolicy({
  directives: {
    scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
  }
}));

// Inject nonce into Angular's index.html
app.get('*', (req, res) => {
  let html = fs.readFileSync('index.html', 'utf-8');
  html = html.replace(/<script/g, `<script nonce="${res.locals.nonce}"`);
  res.send(html);
});

Authentication Patterns

JWT Authentication

Token storage matters more than you think. Storing JWTs in localStorage is convenient but dangerous — any XSS vulnerability gives an attacker permanent access to the token. Storing the access token in memory (a JavaScript variable/signal) and the refresh token in an httpOnly cookie is the most secure pattern. The trade-off: the user must re-authenticate if they refresh the page (unless you use a refresh token flow). For most applications, this is worth the security improvement.
// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  
  // Access token stored in MEMORY only -- not localStorage, not sessionStorage.
  // This means XSS cannot steal the token (no persistent storage to read from).
  private currentUser = signal<User | null>(null);
  private token = signal<string | null>(null);
  
  readonly isAuthenticated = computed(() => !!this.token());
  readonly user = this.currentUser.asReadonly();
  
  constructor() {
    // Restore from secure storage on init
    this.loadStoredAuth();
  }
  
  async login(credentials: LoginCredentials): Promise<void> {
    const response = await firstValueFrom(
      this.http.post<AuthResponse>('/api/auth/login', credentials)
    );
    
    this.setAuth(response);
    this.router.navigate(['/dashboard']);
  }
  
  logout(): void {
    this.clearAuth();
    this.router.navigate(['/login']);
  }
  
  refreshToken(): Observable<AuthResponse> {
    return this.http.post<AuthResponse>('/api/auth/refresh', {}).pipe(
      tap(response => this.setAuth(response)),
      catchError(error => {
        this.logout();
        return throwError(() => error);
      })
    );
  }
  
  private setAuth(response: AuthResponse): void {
    this.token.set(response.accessToken);
    this.currentUser.set(response.user);
    
    // Store refresh token in httpOnly cookie (set by server)
    // Store access token in memory only (not localStorage!)
  }
  
  private clearAuth(): void {
    this.token.set(null);
    this.currentUser.set(null);
  }
  
  private loadStoredAuth(): void {
    // Only restore if using secure httpOnly cookies for refresh
    // Access token should be fetched fresh via refresh endpoint
  }
  
  getToken(): string | null {
    return this.token();
  }
}

Auth Interceptor

// auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();
  
  // Skip auth for certain endpoints
  if (req.url.includes('/auth/') || req.url.includes('/public/')) {
    return next(req);
  }
  
  if (token) {
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
  
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        // Token expired, try refresh
        return authService.refreshToken().pipe(
          switchMap(() => {
            // Retry with new token
            const newToken = authService.getToken();
            const retryReq = req.clone({
              setHeaders: {
                Authorization: `Bearer ${newToken}`
              }
            });
            return next(retryReq);
          })
        );
      }
      return throwError(() => error);
    })
  );
};

Route Guards

// auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isAuthenticated()) {
    return true;
  }
  
  // Store attempted URL for redirect after login
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// role.guard.ts
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
  return (route, state) => {
    const authService = inject(AuthService);
    const router = inject(Router);
    
    const user = authService.user();
    
    if (!user) {
      return router.createUrlTree(['/login']);
    }
    
    if (!allowedRoles.includes(user.role)) {
      return router.createUrlTree(['/unauthorized']);
    }
    
    return true;
  };
};

// Usage
export const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
  { 
    path: 'admin', 
    component: AdminComponent, 
    canActivate: [authGuard, roleGuard(['admin'])]
  }
];

Secure HTTP Practices

HTTPS Enforcement

// Force HTTPS in production
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});

// HSTS header
app.use(helmet.hsts({
  maxAge: 31536000,  // 1 year
  includeSubDomains: true,
  preload: true
}));
// Server-side cookie settings
res.cookie('session', sessionId, {
  httpOnly: true,     // Not accessible via JavaScript
  secure: true,       // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 86400000,   // 24 hours
  path: '/'
});

API Security Headers

// Recommended headers
app.use(helmet({
  contentSecurityPolicy: { ... },
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: { policy: 'same-origin' },
  crossOriginResourcePolicy: { policy: 'same-origin' },
  dnsPrefetchControl: { allow: false },
  frameguard: { action: 'deny' },
  hidePoweredBy: true,
  hsts: { maxAge: 31536000, includeSubDomains: true },
  ieNoOpen: true,
  noSniff: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xssFilter: true
}));

Input Validation

Client-Side Validation

// validators.ts
export const Validators = {
  email: (control: AbstractControl) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(control.value) ? null : { email: true };
  },
  
  strongPassword: (control: AbstractControl) => {
    const value = control.value;
    const errors: any = {};
    
    if (!/[A-Z]/.test(value)) errors.uppercase = true;
    if (!/[a-z]/.test(value)) errors.lowercase = true;
    if (!/[0-9]/.test(value)) errors.number = true;
    if (!/[!@#$%^&*]/.test(value)) errors.special = true;
    if (value.length < 12) errors.minLength = true;
    
    return Object.keys(errors).length ? { weakPassword: errors } : null;
  },
  
  noScript: (control: AbstractControl) => {
    const dangerous = /<script|javascript:|on\w+=/i;
    return dangerous.test(control.value) ? { dangerous: true } : null;
  },
  
  sanitizedInput: (control: AbstractControl) => {
    // Only allow alphanumeric and basic punctuation
    const safe = /^[a-zA-Z0-9\s.,!?'-]*$/;
    return safe.test(control.value) ? null : { unsafeCharacters: true };
  }
};

// Usage
this.form = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  password: ['', [Validators.required, CustomValidators.strongPassword]],
  username: ['', [Validators.required, CustomValidators.sanitizedInput]]
});

Server-Side Validation (Always Required!)

Client-side validation is a courtesy. Server-side validation is security. A user can disable JavaScript, modify HTTP requests with a tool like Burp Suite, or call your API directly with curl. Every validation rule you write in Angular must also exist on the server. Think of client-side validation as instant feedback for honest users, and server-side validation as the actual gate that stops bad data.
// Express validation with express-validator
import { body, validationResult } from 'express-validator';

app.post('/api/users',
  body('email').isEmail().normalizeEmail(),
  body('password').isStrongPassword({
    minLength: 12,
    minLowercase: 1,
    minUppercase: 1,
    minNumbers: 1,
    minSymbols: 1
  }),
  body('username').trim().escape().isLength({ min: 3, max: 20 }),
  
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process valid request
  }
);

Security Checklist

┌─────────────────────────────────────────────────────────────────────────┐
│              Security Checklist                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   XSS Prevention:                                                        │
│   □ Never bypass Angular's sanitization for user input                  │
│   □ Use [innerHTML] only with sanitized content                         │
│   □ Validate/sanitize on server side too                                │
│   □ Implement Content Security Policy                                    │
│                                                                          │
│   Authentication:                                                        │
│   □ Use httpOnly cookies for tokens when possible                       │
│   □ Implement token refresh flow                                        │
│   □ Use secure password hashing (bcrypt/argon2)                         │
│   □ Implement rate limiting on auth endpoints                           │
│   □ Use MFA for sensitive applications                                  │
│                                                                          │
│   Authorization:                                                         │
│   □ Implement route guards for protected pages                          │
│   □ Check permissions on both client and server                         │
│   □ Don't rely on client-side checks alone                              │
│                                                                          │
│   HTTPS/Transport:                                                       │
│   □ Enforce HTTPS in production                                         │
│   □ Use HSTS headers                                                     │
│   □ Configure secure cookie options                                     │
│   □ Implement proper CORS settings                                      │
│                                                                          │
│   General:                                                               │
│   □ Keep dependencies updated (npm audit)                               │
│   □ Use environment variables for secrets                               │
│   □ Implement proper error handling (no stack traces to client)         │
│   □ Log security events                                                  │
│   □ Implement input validation everywhere                               │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Practice Exercise

Exercise: Implement Secure Authentication Flow

Build a secure authentication system with:
  1. Login/logout functionality
  2. JWT token handling (access + refresh)
  3. Protected routes
  4. Role-based access control
  5. XSS-safe user profile display
// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  
  private _user = signal<User | null>(null);
  private _token = signal<string | null>(null);
  private refreshTokenTimeout?: ReturnType<typeof setTimeout>;
  
  readonly user = this._user.asReadonly();
  readonly isAuthenticated = computed(() => !!this._token());
  readonly hasRole = (role: string) => computed(() => 
    this._user()?.roles.includes(role) ?? false
  );
  
  async login(email: string, password: string): Promise<void> {
    try {
      const response = await firstValueFrom(
        this.http.post<AuthResponse>('/api/auth/login', { email, password }, {
          withCredentials: true  // Include cookies
        })
      );
      
      this.handleAuthSuccess(response);
    } catch (error) {
      this.handleAuthError(error);
      throw error;
    }
  }
  
  async logout(): Promise<void> {
    try {
      await firstValueFrom(
        this.http.post('/api/auth/logout', {}, { withCredentials: true })
      );
    } finally {
      this.clearAuth();
      this.router.navigate(['/login']);
    }
  }
  
  refreshToken(): Observable<AuthResponse> {
    return this.http.post<AuthResponse>('/api/auth/refresh', {}, {
      withCredentials: true
    }).pipe(
      tap(response => this.handleAuthSuccess(response)),
      catchError(error => {
        this.clearAuth();
        return throwError(() => error);
      })
    );
  }
  
  private handleAuthSuccess(response: AuthResponse): void {
    this._token.set(response.accessToken);
    this._user.set(response.user);
    this.startRefreshTimer(response.expiresIn);
  }
  
  private startRefreshTimer(expiresIn: number): void {
    // Refresh 1 minute before expiry
    const refreshTime = (expiresIn - 60) * 1000;
    
    this.clearRefreshTimer();
    this.refreshTokenTimeout = setTimeout(() => {
      this.refreshToken().subscribe();
    }, refreshTime);
  }
  
  private clearRefreshTimer(): void {
    if (this.refreshTokenTimeout) {
      clearTimeout(this.refreshTokenTimeout);
    }
  }
  
  private clearAuth(): void {
    this._token.set(null);
    this._user.set(null);
    this.clearRefreshTimer();
  }
  
  private handleAuthError(error: any): void {
    console.error('Auth error:', error);
  }
  
  getToken(): string | null {
    return this._token();
  }
}

// auth.guard.ts
export const authGuard: CanActivateFn = async (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isAuthenticated()) {
    return true;
  }
  
  // Try to refresh token
  try {
    await firstValueFrom(authService.refreshToken());
    return true;
  } catch {
    return router.createUrlTree(['/login'], {
      queryParams: { returnUrl: state.url }
    });
  }
};

export const roleGuard = (roles: string[]): CanActivateFn => {
  return (route, state) => {
    const authService = inject(AuthService);
    const router = inject(Router);
    
    const user = authService.user();
    if (!user) {
      return router.createUrlTree(['/login']);
    }
    
    const hasRequiredRole = roles.some(role => user.roles.includes(role));
    if (!hasRequiredRole) {
      return router.createUrlTree(['/forbidden']);
    }
    
    return true;
  };
};

// user-profile.component.ts
@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [SanitizeHtmlPipe],
  template: `
    <div class="profile">
      <!-- Safe: automatically escaped -->
      <h1>{{ user()?.name }}</h1>
      <p>{{ user()?.email }}</p>
      
      <!-- Safe: sanitized through pipe -->
      <div 
        class="bio"
        [innerHTML]="user()?.bio | sanitizeHtml"
      ></div>
      
      <!-- Safe: Angular sanitizes URLs -->
      <img [src]="user()?.avatar" alt="Avatar" />
      
      <!-- Roles display -->
      <div class="roles">
        @for (role of user()?.roles; track role) {
          <span class="badge">{{ role }}</span>
        }
      </div>
    </div>
  `
})
export class UserProfileComponent {
  private authService = inject(AuthService);
  user = this.authService.user;
}

// routes
export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { 
    path: 'dashboard', 
    component: DashboardComponent,
    canActivate: [authGuard]
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, roleGuard(['admin'])]
  },
  { path: 'forbidden', component: ForbiddenComponent }
];

Summary

1

XSS Protection

Angular sanitizes by default; never bypass for user input
2

CSRF Protection

Use Angular’s built-in XSRF support with proper server setup
3

Authentication

Implement secure token handling with refresh flow
4

Authorization

Use route guards and verify permissions server-side
5

Input Validation

Validate on both client and server; never trust client

Interview Deep-Dive

Strong Answer: bypassSecurityTrustHtml disables Angular’s XSS sanitization entirely for that content. If the CMS stores user-submitted HTML containing a script tag or an onerror handler, that code executes in every user’s browser — a textbook stored XSS vulnerability.The safe approach: sanitize on the server when content is submitted (DOMPurify, sanitize-html). Sanitize on the client using an allowlist of safe tags (p, strong, em, a, ul, li) and safe attributes (href, class, alt). Implement a Content Security Policy header that blocks inline script execution as defense-in-depth.Angular’s built-in sanitizer on [innerHTML] removes script tags and event handlers by default. For high-security apps, add DOMPurify on top, as it is actively maintained against the latest XSS vectors.Follow-up: Is [innerHTML] without bypass safe enough? Answer: Angular’s sanitizer is good but not perfect — edge cases with obfuscated HTML exist. For anything that renders user-generated content, I add DOMPurify with a strict tag allowlist in addition to Angular’s sanitizer. Belt and suspenders.
Strong Answer: localStorage is persistent and accessible via JavaScript — vulnerable to XSS. httpOnly cookies are not accessible via JavaScript — resistant to XSS but vulnerable to CSRF (mitigated with SameSite=Strict and CSRF tokens). In-memory storage (a signal in AuthService) is cleared on refresh and not accessible to XSS, but requires re-authentication on page reload.My recommended approach: refresh token in an httpOnly, Secure, SameSite=Strict cookie set by the server. Short-lived access token in memory only. On page load, silently call the refresh endpoint. This resists both XSS (access token not in storage) and CSRF (SameSite=Strict).Follow-up: How do you handle multiple tabs? Answer: Each tab gets its own in-memory access token via the shared refresh cookie. For coordinating logout across tabs, use the BroadcastChannel API — when one tab logs out, it broadcasts a “logout” event that other tabs receive.
Strong Answer: Nothing went wrong with the guards — they are a UX feature, not a security feature. Guards run in the browser and can be trivially bypassed. The real issue is missing server-side authorization on the API endpoints.Every API endpoint must independently verify the user’s identity and permissions. The JWT should contain the user’s role, and the server must check it on every request. Route guards redirect unauthorized users to a clean “forbidden” page instead of showing a broken admin UI. They do not prevent access — they prevent confusion.Follow-up: What about hiding admin UI elements with @if based on role? Answer: Also UX, not security. The HTML is not rendered, but all component code IS in the JavaScript bundle. For true code-level protection, lazy-load admin features behind a route with canMatch that checks the role — the admin JavaScript is not even downloaded unless the JWT proves admin status. But the API must still enforce authorization independently.

Next Steps

Next: Capstone Project

Apply everything you’ve learned in a comprehensive project