Lesson 14-Angular Security and Permissions

Security Mechanisms

XSS Protection

Content Security Policy (CSP) Configuration:

<!-- Set CSP meta tag in index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted.cdn.com; 
               style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; 
               img-src 'self' data: https://*.example.com">

DOM Sanitization Implementation:

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

@Component({...})
export class SafeContentComponent {
  safeHtml: SafeHtml;

  constructor(private sanitizer: DomSanitizer) {}

  sanitizeContent(rawHtml: string) {
    // Use DomSanitizer for sanitization
    this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(rawHtml);

    // Safer approach with custom sanitization logic
    // this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(
    //   this.customSanitizer.sanitize(rawHtml)
    // );
  }
}

XSS Protection Best Practices:

  1. Never use innerHTML to insert user input directly
  2. Use Angular’s data binding syntax {{ }} for automatic content escaping
  3. Strictly use DomSanitizer for scenarios requiring HTML insertion
  4. Avoid using eval(), new Function(), or other dynamic code execution

CSRF Protection

HttpInterceptor for CSRF Protection:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Exclude requests that don't require CSRF protection (e.g., GET, HEAD, OPTIONS)
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next.handle(req);
    }

    // Retrieve CSRF token (typically from cookie)
    const csrfToken = this.getCookie('XSRF-TOKEN');

    // Clone request and add CSRF header
    if (csrfToken) {
      const securedReq = req.clone({
        headers: req.headers.set('X-XSRF-TOKEN', csrfToken)
      });
      return next.handle(securedReq);
    }

    return next.handle(req);
  }

  private getCookie(name: string): string | null {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
    return null;
  }
}

// Register interceptor in AppModule
@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true }
  ]
})
export class AppModule {}

CSRF Protection Configuration:

  1. Ensure the backend generates and sets the XSRF-TOKEN cookie
  2. Frontend retrieves the token from the cookie and adds it to the request header
  3. Backend verifies that the token in the request header matches the cookie token

Route Guards and Permission Control

Role-Based Route Guard:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class RoleGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    const requiredRoles = route.data['roles'] as string[];
    const userRoles = this.authService.getUserRoles();

    if (this.authService.isLoggedIn() && requiredRoles.some(role => userRoles.includes(role))) {
      return true;
    }

    // Redirect to login or unauthorized page
    this.router.navigate(['/unauthorized']);
    return false;
  }
}

// Use guard in route configuration
const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [RoleGuard],
    data: { roles: ['admin', 'superadmin'] }
  }
];

Route Guard Types:

  1. CanActivate: Determines if a route can be activated
  2. CanDeactivate: Determines if a route can be deactivated
  3. Resolve: Fetches data before route activation
  4. CanLoad: Determines if a feature module can be loaded

Sensitive Data Encryption and Storage

Encrypted Storage for Sensitive Data:

import { Injectable } from '@angular/core';
import { CryptoService } from './crypto.service';

@Injectable({
  providedIn: 'root'
})
export class SecureStorageService {
  constructor(private cryptoService: CryptoService) {}

  setItem(key: string, value: string): void {
    const encryptedValue = this.cryptoService.encrypt(value);
    localStorage.setItem(key, encryptedValue);
  }

  getItem(key: string): string | null {
    const encryptedValue = localStorage.getItem(key);
    if (!encryptedValue) return null;
    return this.cryptoService.decrypt(encryptedValue);
  }

  removeItem(key: string): void {
    localStorage.removeItem(key);
  }
}

// Encryption service using Web Crypto API
@Injectable({
  providedIn: 'root'
})
export class CryptoService {
  private readonly algorithm = { name: 'AES-GCM', length: 256 };
  private readonly key: CryptoKey;

  constructor() {
    this.initKey();
  }

  private async initKey(): Promise<void> {
    const keyMaterial = await window.crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode('my-secret-key-32-chars-long-1234567890'),
      this.algorithm,
      false,
      ['encrypt', 'decrypt']
    );
    this.key = keyMaterial;
  }

  async encrypt(data: string): Promise<string> {
    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(data);
    const ciphertext = await window.crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.key,
      encoded
    );
    return JSON.stringify({
      iv: Array.from(iv),
      ciphertext: Array.from(new Uint8Array(ciphertext))
    });
  }

  async decrypt(encryptedData: string): Promise<string> {
    const { iv, ciphertext } = JSON.parse(encryptedData);
    const decrypted = await window.crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: new Uint8Array(iv) },
      this.key,
      new Uint8Array(ciphertext)
    );
    return new TextDecoder().decode(decrypted);
  }
}

Secure Storage Recommendations:

  1. Avoid storing sensitive data in localStorage/sessionStorage
  2. Use strong encryption when storage is necessary
  3. Consider using HttpOnly, Secure cookies for session tokens
  4. Implement automatic token refresh mechanisms

Security Best Practices

Input Validation Implementation:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <input formControlName="username" placeholder="Username">
      <div *ngIf="userForm.get('username')?.errors?.['required']">
        Username is required
      </div>
      <div *ngIf="userForm.get('username')?.errors?.['minlength']">
        Username must be at least 3 characters
      </div>

      <input formControlName="email" placeholder="Email">
      <div *ngIf="userForm.get('email')?.errors?.['required']">
        Email is required
      </div>
      <div *ngIf="userForm.get('email')?.errors?.['email']">
        Please enter a valid email address
      </div>

      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
  `
})
export class UserFormComponent {
  userForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.userForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]]
    });
  }

  onSubmit() {
    if (this.userForm.valid) {
      // Handle form submission
    }
  }
}

Least Privilege Principle Implementation:

// Implement fine-grained permission checks in service
@Injectable({
  providedIn: 'root'
})
export class PermissionService {
  private userPermissions: string[] = [];

  constructor(private authService: AuthService) {
    this.loadUserPermissions();
  }

  private async loadUserPermissions(): Promise<void> {
    this.userPermissions = await this.authService.getUserPermissions();
  }

  hasPermission(permission: string): boolean {
    return this.userPermissions.includes(permission);
  }

  hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(p => this.hasPermission(p));
  }

  hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(p => this.hasPermission(p));
  }
}

// Use permission service in component
@Component({...})
export class AdminPanelComponent {
  canEditUsers = false;
  canDeleteUsers = false;

  constructor(private permissionService: PermissionService) {
    this.canEditUsers = this.permissionService.hasPermission('user:edit');
    this.canDeleteUsers = this.permissionService.hasPermission('user:delete');
  }
}

Permission Control

Role-Based Access Control (RBAC)

RBAC Model Implementation:

// Role and permission definitions
interface Role {
  name: string;
  permissions: string[];
}

const ROLES: Record<string, Role> = {
  admin: {
    name: 'admin',
    permissions: ['user:create', 'user:read', 'user:update', 'user:delete']
  },
  editor: {
    name: 'editor',
    permissions: ['user:read', 'user:update']
  },
  viewer: {
    name: 'viewer',
    permissions: ['user:read']
  }
};

// Permission check service
@Injectable({
  providedIn: 'root'
})
export class RbacService {
  private userRole: string;

  constructor(private authService: AuthService) {
    this.userRole = this.authService.getUserRole();
  }

  hasPermission(permission: string): boolean {
    const role = ROLES[this.userRole];
    return role?.permissions.includes(permission) || false;
  }

  hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(p => this.hasPermission(p));
  }

  hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(p => this.hasPermission(p));
  }
}

Dynamic Routing and Permission Management

Dynamic Route Generation:

// Route configuration
const APP_ROUTES: Route[] = [
  { path: 'login', component: LoginComponent },
  { path: 'unauthorized', component: UnauthorizedComponent },
  // Other public routes...
];

// Dynamic route service
@Injectable({
  providedIn: 'root'
})
export class DynamicRouteService {
  constructor(
    private router: Router,
    private authService: AuthService
  ) {}

  generateRoutes(): Promise<void> {
    return this.authService.getUserRoles().then(roles => {
      const allowedRoutes = this.getAllowedRoutes(roles);
      this.addRoutesToRouter(allowedRoutes);
    });
  }

  private getAllowedRoutes(roles: string[]): Route[] {
    // Filter routes based on roles
    const roleRoutes: Record<string, Route[]> = {
      admin: [
        { path: 'admin', component: AdminComponent },
        { path: 'users', component: UsersComponent }
      ],
      editor: [
        { path: 'editor', component: EditorComponent }
      ],
      viewer: [
        { path: 'viewer', component: ViewerComponent }
      ]
    };

    // Merge all allowed routes
    return roles.reduce((routes, role) => {
      return routes.concat(roleRoutes[role] || []);
    }, []);
  }

  private addRoutesToRouter(routes: Route[]): void {
    // Get current route configuration
    const currentConfig = this.router.config;

    // Add dynamic routes
    const updatedConfig = [...currentConfig, ...routes];

    // Reset route configuration
    this.router.resetConfig(updatedConfig);
  }
}

// Initialize dynamic routes in AppComponent
@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private dynamicRouteService: DynamicRouteService,
    private router: Router
  ) {}

  ngOnInit(): void {
    this.dynamicRouteService.generateRoutes().then(() => {
      // Check if current route is allowed
      this.checkCurrentRouteAccess();
    });
  }

  private checkCurrentRouteAccess(): void {
    const currentRoute = this.router.url;
    if (!this.isRouteAllowed(currentRoute)) {
      this.router.navigate(['/unauthorized']);
    }
  }

  private isRouteAllowed(route: string): boolean {
    // Implement route access check logic
    return true; // Simplified example
  }
}

Component and Directive Permission Control

Permission Directive Implementation:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { PermissionService } from './permission.service';

@Directive({
  selector: '[appHasPermission]'
})
export class HasPermissionDirective {
  @Input() appHasPermission: string | string[];

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private permissionService: PermissionService
  ) {}

  ngOnInit(): void {
    this.updateView();
  }

  private updateView(): void {
    const permissions = Array.isArray(this.appHasPermission)
      ? this.appHasPermission
      : [this.appHasPermission];

    if (this.permissionService.hasAnyPermission(permissions)) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

// Usage example
@Component({
  template: `
    <button *appHasPermission="'user:create'">Create User</button>
    <div *appHasPermission="['user:read', 'user:update']">
      User Management Content
    </div>
  `
})
export class UserManagementComponent {}

Permission Component Implementation:

import { Component, Input } from '@angular/core';
import { PermissionService } from './permission.service';

@Component({
  selector: 'app-permission-wrapper',
  template: `
    <ng-container *ngIf="hasPermission">
      <ng-content></ng-content>
    </ng-container>
  `
})
export class PermissionWrapperComponent {
  @Input() permission: string | string[];
  hasPermission = false;

  constructor(private permissionService: PermissionService) {}

  ngOnChanges(): void {
    this.updatePermission();
  }

  private updatePermission(): void {
    const permissions = Array.isArray(this.permission)
      ? this.permission
      : [this.permission];

    this.hasPermission = this.permissionService.hasAnyPermission(permissions);
  }
}

// Usage example
@Component({
  template: `
    <app-permission-wrapper [permission]="'user:delete'">
      <button>Delete User</button>
    </app-permission-wrapper>
  `
})
export class UserListComponent {}

API Request Permission Verification

HTTP Interceptor for API Permission Verification:

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // Clone request and add authorization header
    const authReq = this.addAuthHeader(req);

    return next.handle(authReq).pipe(
      catchError((error: HttpErrorResponse) => {
        // Handle 401 unauthorized error
        if (error.status === 401) {
          this.authService.handleUnauthorized();
        }
        // Handle 403 forbidden error
        else if (error.status === 403) {
          this.authService.handleForbidden();
        }
        return throwError(error);
      })
    );
  }

  private addAuthHeader(req: HttpRequest<any>): HttpRequest<any> {
    const token = this.authService.getAccessToken();
    if (token) {
      return req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
    }
    return req;
  }
}

// Register interceptor in AppModule
@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})
export class AppModule {}

API Permission Verification Strategies:

  1. JWT Token: Include JWT token in request headers
  2. OAuth2: Use access tokens for authentication
  3. API Keys: For service-to-service communication
  4. ABAC: Attribute-based access control

Permission Control Performance Optimization

Permission Caching Strategy:

@Injectable({
  providedIn: 'root'
})
export class PermissionService {
  private permissions: string[] = [];
  private permissionsLoaded = false;
  private permissionsSubject = new BehaviorSubject<string[]>([]);
  permissions$ = this.permissionsSubject.asObservable();

  constructor(private authService: AuthService) {}

  async loadPermissions(): Promise<void> {
    if (this.permissionsLoaded) {
      return;
    }

    this.permissions = await this.authService.getUserPermissions();
    this.permissionsSubject.next(this.permissions);
    this.permissionsLoaded = true;
  }

  hasPermission(permission: string): boolean {
    if (!this.permissionsLoaded) {
      // Return false or throw error if permissions not loaded
      return false;
    }
    return this.permissions.includes(permission);
  }

  // Other methods...
}

// Use permission service in route guard
@Injectable({
  providedIn: 'root'
})
export class RoleGuard implements CanActivate {
  constructor(
    private permissionService: PermissionService,
    private router: Router
  ) {}

  async canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> {
    await this.permissionService.loadPermissions();

    const requiredRoles = route.data['roles'] as string[];
    const userRoles = this.permissionService.getUserRoles();

    if (this.authService.isLoggedIn() && requiredRoles.some(role => userRoles.includes(role))) {
      return true;
    }

    this.router.navigate(['/unauthorized']);
    return false;
  }
}

Performance Optimization Strategies:

  1. Permission Preloading: Load permissions during application initialization
  2. Permission Caching: Avoid repeated permission data requests
  3. Lazy Permission Loading: Load permissions for specific features on demand
  4. Change Detection Optimization: Use OnPush change detection strategy
  5. Pure Pipes: Use pure pipes for permission-related display logic

By implementing these comprehensive security and permission controls, Angular applications can establish a secure and reliable user authentication and authorization system, effectively protecting application data and user privacy.

Share your love