For example, to set up routing security in your application with the previous version of Angular, you needed to write a complex class. It required implementing the CanActivate interface, using dependency injection to pass your services to the constructor, and returning a complex RxJS observable to determine if the user was allowed to view the page.
In the latest version of Angular, routing security is functional, tree-shakeable, and tightly integrated with Signals.
If your application is still using class-based routing security or directly using localStorage in your components, your security layer is already vulnerable. But do not worry. Here is the standard for architecting bulletproof route security and working with JWTs in 2026.
1. The Modern Auth Guard
In Standalone, a route guard is simply a function of type CanActivateFn. Since it is running in an injection context, you can use the inject function to inject your AuthService and Router directly.
Additionally, in Angular 18 and later, a new property is available called RedirectCommand. Instead of returning a UrlTree to redirect unauthenticated users, this property allows for finer control over the redirection process (such as replacing the URL so the user cannot use the Back button to return to the previous route).
// The Modern Functional Guard
import { CanActivateFn, RedirectCommand, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
// Read the synchronous Signal state
if (authService.isAuthenticated()) {
return true;
}
// Securely redirect, preventing history pollution
const loginUrl = router.parseUrl('/login');
return new RedirectCommand(loginUrl, {
skipLocationChange: true,
info: { returnUrl: state.url } // Save the attempted URL for post-login
});
};
You apply it directly in your app.routes.ts :
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard.component').then(c => c.DashboardComponent),
canActivate: [authGuard]
}
];
2. Role-Based Access Control (Factory Guards)
But what about the situation where you want the guard to check for certain roles? The CanActivateFn type doesn’t support custom arguments.
In order to pass arguments to the functional guard, we can utilize the Factory Function. We create a function that accepts our configuration object, such as an array of roles, and returns the CanActivateFn.
// The Factory Guard Pattern
export function requireRoles(allowedRoles: string[]): CanActivateFn {
return () => {
const authService = inject(AuthService);
const router = inject(Router);
// Assume role() is a Signal containing the user's current role
const currentRole = authService.role();
if (currentRole && allowedRoles.includes(currentRole)) {
return true;
}
return router.parseUrl('/unauthorized');
};
}
This makes your route configuration incredibly declarative and easy to read:
export const routes: Routes = [
{
path: 'admin-panel',
loadComponent: () => import('./admin.component').then(c => c.AdminComponent),
// Execute the factory to generate the guard
canActivate: [requireRoles(['ADMIN', 'SUPER_ADMIN'])]
}
];
3. JWT Handling: Trust, but Verify
While the guard is used to protect the UI, the reality is that the UI is an illusion. An unscrupulous user can bypass your Angular router as easily as pie by altering the compiled JavaScript in the browser to ensure that isAuthenticated() always returns true.
The Golden Rule: Frontend security is strictly for User Experience. Real security happens on the Backend.
But the frontend still must utilize the JSON Web Token intelligently to avoid unnecessary, failing API calls.
Never send the token blindly with the request. You must decode the token and verify expiration locally before sending the token.
// auth.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { jwtDecode } from 'jwt-decode';
@Injectable({ providedIn: 'root' })
export class AuthService {
// Store the raw token in a Signal
private token = signal<string | null>(localStorage.getItem('jwt'));
// Automatically derive the decoded payload
private decodedToken = computed(() => {
const t = this.token();
if (!t) return null;
try {
return jwtDecode(t);
} catch {
return null;
}
});
// Automatically derive the authentication state based on expiration
isAuthenticated = computed(() => {
const decoded = this.decodedToken();
if (!decoded || !decoded.exp) return false;
// Check if the token is expired (exp is in seconds, Date.now is in ms)
const isExpired = (decoded.exp * 1000) < Date.now();
return !isExpired;
});
}
By leveraging Signals, the moment the token changes (i.e., a successful login or a silent refresh), the decodedToken and isAuthenticated state are recalculated synchronously throughout your entire application.
4. The Token Leakage Trap
When writing your functional HTTP Interceptor to attach the JWT, developers often write a blanket rule: “Attach the token to every outgoing request.”
This is a critical security vulnerability.
For example, if your Angular app makes a call to a third-party API, such as Cloudinary for image uploads or Stripe for payment processing, a blanket interceptor will add your user’s primary JWT token to this third-party request. Congratulations, you have just sent your users’ credentials to a third-party server.
Always whitelist your domains.
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
// ONLY attach the token if the request is going to YOUR secure backend
const isApiUrl = req.url.startsWith('https://api.mycompany.com');
if (token && isApiUrl) {
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
}));
}
return next(req);
};
Summary
Security in Modern Angular is lightweight and mathematically predictable with the help of Signals and functional APIs.
- Remove your CanActivate classes. Use CanActivateFn.
- Control Navigation securely using RedirectCommand to control the browser history.
- Utilize Factory Functions to directly pass role arrays into your route guards.
- Derive State by decoding your JWT token locally using the computed() signal to prevent making API calls with expired tokens.
- Whitelist Domains in your interceptor to prevent leaking Bearer tokens to third-party services.
Comments
Loading comments…