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
| Practice | Details |
|---|---|
| ✅ Store tokens in HttpOnly cookies | Not accessible via JavaScript (XSS-safe) |
| ✅ Use short-lived access tokens | 15 min access, 7-day refresh |
| ✅ Validate on backend | Never trust client-side guards for security |
| ✅ Sanitize all user input | Angular does this by default — don't bypass |
| ✅ Use CSP headers | Prevent script injection |
| ✅ HTTPS everywhere | TLS for all API communication |
| ❌ Don't store tokens in localStorage | Vulnerable to XSS |
❌ Don't use bypassSecurityTrust* with user input | XSS vulnerability |
| ❌ Don't expose secrets in frontend | API keys, secrets belong on the server |
Key Takeaways
- Use HttpOnly cookies for token storage —
localStorageis 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-clientfor OAuth/OIDC (Google, Azure AD, Auth0, Keycloak) - Set CSP headers to prevent script injection attacks