Skip to main content
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. 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:

Automatic Sanitization

@Component({
  template: `
    <!-- SAFE: Automatically escaped -->
    <p>{{ userInput }}</p>
    <div [innerHTML]="htmlContent"></div>
    <a [href]="userUrl">Link</a>
    <img [src]="imageUrl" />
  `
})
export class SafeComponent {
  // Even malicious input is escaped
  userInput = '<script>alert("XSS")</script>';  // Rendered as text
  
  // HTML is sanitized, scripts removed
  htmlContent = '<p>Safe</p><script>alert("XSS")</script>';  // Script removed
  
  // Dangerous URLs blocked
  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 for content from trusted sources that you control.

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

HttpClient XSRF Support

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',     // Read token from this cookie
        headerName: 'X-XSRF-TOKEN'    // Send token in this header
      })
    )
  ]
};
// 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

// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  
  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!)

// 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

Next Steps

Next: Capstone Project

Apply everything you’ve learned in a comprehensive project