Docs
/
Angular
Chapter 18

18 — Authentication & Security

Authentication Flow

1. User submits credentials (login form)
2. Backend validates → returns JWT (access + refresh tokens)
3. Frontend stores tokens (HttpOnly cookie or memory)
4. Every API request includes the token (interceptor)
5. Guards protect routes based on auth state
6. Refresh token rotates before access token expires

Auth Service

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);

  private currentUser = signal<User | null>(null);
  private accessToken = signal<string | null>(null);

  readonly user = this.currentUser.asReadonly();
  readonly isAuthenticated = computed(() => !!this.currentUser());
  readonly isAdmin = computed(() => this.currentUser()?.role === 'admin');

  login(credentials: LoginDto): Observable<AuthResponse> {
    return this.http.post<AuthResponse>('/api/auth/login', credentials).pipe(
      tap(res => {
        this.accessToken.set(res.accessToken);
        this.currentUser.set(res.user);
        // Store refresh token in HttpOnly cookie (set by backend)
      }),
    );
  }

  logout() {
    this.http.post('/api/auth/logout', {}).subscribe();
    this.accessToken.set(null);
    this.currentUser.set(null);
    this.router.navigate(['/login']);
  }

  refreshToken(): Observable<AuthResponse> {
    return this.http.post<AuthResponse>('/api/auth/refresh', {}).pipe(
      tap(res => {
        this.accessToken.set(res.accessToken);
        this.currentUser.set(res.user);
      }),
    );
  }

  getToken(): string | null {
    return this.accessToken();
  }

  hasRole(role: string): boolean {
    return this.currentUser()?.role === role;
  }
}

Auth Interceptor (JWT)

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  // Skip auth for public endpoints
  if (req.url.includes('/auth/login') || req.url.includes('/auth/register')) {
    return next(req);
  }

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req).pipe(
    catchError(error => {
      if (error.status === 401) {
        // Try refresh
        return authService.refreshToken().pipe(
          switchMap(res => {
            const retryReq = req.clone({
              setHeaders: { Authorization: `Bearer ${res.accessToken}` },
            });
            return next(retryReq);
          }),
          catchError(() => {
            authService.logout();
            return throwError(() => error);
          }),
        );
      }
      return throwError(() => error);
    }),
  );
};

Route Guards

// Auth guard — must be logged in
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isAuthenticated()) return true;
  return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};

// Role guard — must have specific role
export const roleGuard: CanActivateFn = (route) => {
  const auth = inject(AuthService);
  const requiredRoles = route.data['roles'] as string[];
  if (requiredRoles.some(r => auth.hasRole(r))) return true;
  return inject(Router).createUrlTree(['/unauthorized']);
};

// No-auth guard — redirect if already logged in
export const noAuthGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  if (!auth.isAuthenticated()) return true;
  return inject(Router).createUrlTree(['/dashboard']);
};
// Route config
{
  path: 'login',
  canActivate: [noAuthGuard],
  loadComponent: () => import('./auth/login.component'),
},
{
  path: 'dashboard',
  canActivate: [authGuard],
  loadComponent: () => import('./dashboard/dashboard.component'),
},
{
  path: 'admin',
  canActivate: [authGuard, roleGuard],
  data: { roles: ['admin', 'superadmin'] },
  loadChildren: () => import('./admin/admin.routes'),
},

OAuth / OIDC with angular-auth-oidc-client

npm install angular-auth-oidc-client
// app.config.ts
import { provideAuth, withDefaultFeatures } from 'angular-auth-oidc-client';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAuth({
      config: {
        authority: 'https://auth.example.com',
        redirectUrl: window.location.origin,
        postLogoutRedirectUri: window.location.origin,
        clientId: 'my-angular-app',
        scope: 'openid profile email',
        responseType: 'code',
        silentRenew: true,
        useRefreshToken: true,
      },
    }),
  ],
};
// Usage
@Component({ ... })
export class AppComponent implements OnInit {
  private oidcService = inject(OidcSecurityService);

  isAuthenticated = toSignal(
    this.oidcService.isAuthenticated$.pipe(map(r => r.isAuthenticated)),
    { initialValue: false },
  );

  ngOnInit() {
    this.oidcService.checkAuth().subscribe();
  }

  login()  { this.oidcService.authorize(); }
  logout() { this.oidcService.logoff().subscribe(); }
}

XSS Prevention

Angular automatically sanitizes values bound to the DOM.

<!-- ✅ Safe — Angular escapes HTML -->
<p>{{ userInput }}</p>

<!-- ⚠️ Bypasses sanitization — only use with TRUSTED content -->
<div [innerHTML]="trustedHtml"></div>
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

private sanitizer = inject(DomSanitizer);

// Only bypass when you TRUST the source (e.g., your own CMS)
trustedHtml: SafeHtml = this.sanitizer.bypassSecurityTrustHtml(
  '<strong>Bold text</strong>',
);

// ❌ NEVER do this with user input — XSS vulnerability!
// this.sanitizer.bypassSecurityTrustHtml(req.body.content);

CSRF Protection

// Angular's HttpClient automatically reads XSRF-TOKEN cookie
// and sets X-XSRF-TOKEN header

// app.config.ts
provideHttpClient(
  withXsrfConfiguration({
    cookieName: 'XSRF-TOKEN',    // Cookie name set by backend
    headerName: 'X-XSRF-TOKEN',  // Header sent to backend
  }),
),

Content Security Policy (CSP)

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
  content="
    default-src 'self';
    script-src 'self';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com;
    font-src 'self' https://fonts.gstatic.com;
  ">

Security Best Practices

PracticeDetails
✅ Store tokens in HttpOnly cookiesNot accessible via JavaScript (XSS-safe)
✅ Use short-lived access tokens15 min access, 7-day refresh
✅ Validate on backendNever trust client-side guards for security
✅ Sanitize all user inputAngular does this by default — don't bypass
✅ Use CSP headersPrevent script injection
✅ HTTPS everywhereTLS for all API communication
❌ Don't store tokens in localStorageVulnerable to XSS
❌ Don't use bypassSecurityTrust* with user inputXSS vulnerability
❌ Don't expose secrets in frontendAPI keys, secrets belong on the server

Key Takeaways

  • Use HttpOnly cookies for token storage — localStorage is vulnerable to XSS
  • Auth interceptor attaches the token to every request and handles 401 refresh
  • Route guards are for UX (hide routes), not security — always validate on the backend
  • Angular sanitizes template bindings by default — never bypass with untrusted content
  • Use angular-auth-oidc-client for OAuth/OIDC (Google, Azure AD, Auth0, Keycloak)
  • Set CSP headers to prevent script injection attacks