1. Introduction
Angular provides a powerful Router module for navigating between views in a single-page application. However, in modern applications, not all routes should be accessible to every user.
Some routes may be restricted to:
- Authenticated users
- Users with specific roles
- Administrators or editors
Angular solves this problem using route guards, specifically the CanActivate guard. CanActivate allows developers to control access to routes by evaluating certain conditions before navigation occurs.
This post provides an in-depth guide to protecting routes using CanActivate, including authentication, role-based access, multi-guard setups, asynchronous checks, best practices, and real-world examples.
2. Understanding CanActivate Guard
The CanActivate interface defines a guard that determines whether a route can be activated.
Core Features
- Runs before a route is entered
- Can return:
boolean
– synchronous checkObservable<boolean>
– asynchronous checkPromise<boolean>
– asynchronous check with promise
- Prevents unauthorized navigation
Typical Use Cases
- Authentication – allow only logged-in users
- Authorization – restrict by role
- Conditional navigation – prevent access under certain application states
3. Creating the AuthGuard
The first step is to create a guard to check authentication.
Step 1 – AuthService
@Injectable({ providedIn: 'root' })
export class AuthService {
private token: string | null = null;
login(token: string) {
this.token = token;
localStorage.setItem('token', token);
}
logout() {
this.token = null;
localStorage.removeItem('token');
}
isLoggedIn(): boolean {
return !!this.token || !!localStorage.getItem('token');
}
}
Step 2 – AuthGuard
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.authService.isLoggedIn()) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}
Explanation:
- Checks if the user is logged in using AuthService.
- Redirects unauthorized users to the
/login
route. - Returns
true
orfalse
to allow or prevent navigation.
4. Role-Based Route Protection
Often, authentication alone is insufficient. Certain routes require specific roles.
Step 1 – Extending AuthService
getUserRole(): string {
// Example: decode JWT or fetch role from API
return 'admin'; // hardcoded for demonstration
}
Step 2 – RoleGuard
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
const role = this.authService.getUserRole();
if (role === 'admin') {
return true;
} else {
this.router.navigate(['/unauthorized']);
return false;
}
}
}
Explanation:
- Ensures the user has the required role (
admin
) before navigation. - Redirects unauthorized users to
/unauthorized
.
5. Applying Multiple Guards
You can apply multiple guards to a single route. Angular evaluates them in order. Navigation proceeds only if all guards return true
.
Route Example
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard]
},
{ path: 'login', component: LoginComponent },
{ path: 'unauthorized', component: UnauthorizedComponent }
];
Explanation:
AuthGuard
ensures the user is authenticated.RoleGuard
ensures the user has the required role.- Unauthorized users are redirected appropriately.
6. Asynchronous Guards
In real-world apps, user authentication and role verification often require backend calls. CanActivate supports asynchronous checks using Observables or Promises.
Example with Observable
canActivate(): Observable<boolean> {
return this.authService.validateToken().pipe(
map(isValid => {
if (isValid) return true;
this.router.navigate(['/login']);
return false;
})
);
}
AuthService validateToken()
validateToken(): Observable<boolean> {
return this.http.get<{ valid: boolean }>('/api/validate-token')
.pipe(map(res => res.valid));
}
Explanation:
- The guard waits for the Observable to resolve.
- If the token is valid, navigation proceeds; otherwise, it redirects.
7. Protecting Child Routes
CanActivateChild
allows protecting all child routes of a parent route.
Example
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivateChild: [AuthGuard],
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];
Explanation:
- The AuthGuard is executed whenever a user navigates to
/admin/users
or/admin/settings
. - Reduces repetitive guard definitions for child routes.
8. Using Guards with Lazy-Loaded Modules
CanActivate works seamlessly with lazy-loaded modules.
Example
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthGuard, RoleGuard]
}
Explanation:
- Guards execute before the module loads.
- Prevents unauthorized users from even loading heavy modules.
9. Redirecting Unauthorized Users
Redirecting users improves UX and communicates access restrictions clearly.
Example
canActivate(): boolean {
if (this.authService.isLoggedIn()) {
return true;
} else {
this.router.navigate(['/login'], { queryParams: { returnUrl: this.router.url } });
return false;
}
}
Explanation:
returnUrl
allows redirecting the user back after login.- Prevents frustration for users who are authorized after signing in.
10. Testing Guards
Testing guards ensures routes are properly protected.
Unit Test Example
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('AuthGuard', () => {
let guard: AuthGuard;
let routerSpy = { navigate: jasmine.createSpy('navigate') };
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthGuard,
{ provide: AuthService, useValue: { isLoggedIn: () => false } },
{ provide: Router, useValue: routerSpy }
]
});
guard = TestBed.inject(AuthGuard);
});
it('should redirect unauthorized users', () => {
expect(guard.canActivate()).toBe(false);
expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']);
});
});
11. Best Practices for CanActivate Guards
- Keep logic simple – Guards should only handle navigation checks.
- Combine with services – Centralize authentication and role checks.
- Use asynchronous guards for backend validation.
- Apply multiple guards for layered security.
- Redirect unauthorized users to login or unauthorized pages.
- Test guards thoroughly to avoid accidental access.
- Leverage CanActivateChild for nested routes to reduce repetition.
12. Full Example: Combining AuthGuard and RoleGuard
AuthService
@Injectable({ providedIn: 'root' })
export class AuthService {
private token: string | null = null;
login(token: string) { localStorage.setItem('token', token); this.token = token; }
logout() { localStorage.removeItem('token'); this.token = null; }
isLoggedIn(): boolean { return !!this.token || !!localStorage.getItem('token'); }
getUserRole(): string { return 'admin'; }
}
AuthGuard
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.authService.isLoggedIn()) return true;
this.router.navigate(['/login']);
return false;
}
}
RoleGuard
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.authService.getUserRole() === 'admin') return true;
this.router.navigate(['/unauthorized']);
return false;
}
}
Routes
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard]
},
{ path: 'login', component: LoginComponent },
{ path: 'unauthorized', component: UnauthorizedComponent }
];
Leave a Reply