Module Overview
Estimated Time: 3-4 hours | Difficulty: Advanced | Prerequisites: Module 13
- XSS protection
- CSRF/XSRF protection
- Content Security Policy
- Authentication patterns
- Secure HTTP practices
- Input validation
Angular Security Model
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
@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!)
Copy
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
Copy
// 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
Copy
// 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
})
)
]
};
Copy
// 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
Copy
// 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)
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
}));
Secure Cookie Configuration
Copy
// 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
Copy
// 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
Copy
// 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!)
Copy
// 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
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ 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:
- Login/logout functionality
- JWT token handling (access + refresh)
- Protected routes
- Role-based access control
- XSS-safe user profile display
Solution
Solution
Copy
// 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