Protecting Routes with CanActivate in Angular

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 check
    • Observable<boolean> – asynchronous check
    • Promise<boolean> – asynchronous check with promise
  • Prevents unauthorized navigation

Typical Use Cases

  1. Authentication – allow only logged-in users
  2. Authorization – restrict by role
  3. 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(&#91;'/login']);
  return false;
}
} }

Explanation:

  • Checks if the user is logged in using AuthService.
  • Redirects unauthorized users to the /login route.
  • Returns true or false 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(&#91;'/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: &#91;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 =&gt; {
  if (isValid) return true;
  this.router.navigate(&#91;'/login']);
  return false;
})
); }

AuthService validateToken()

validateToken(): Observable<boolean> {
  return this.http.get<{ valid: boolean }>('/api/validate-token')
.pipe(map(res =&gt; 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: &#91;AuthGuard],
children: &#91;
  { 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(&#91;'/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: &#91;
    AuthGuard,
    { provide: AuthService, useValue: { isLoggedIn: () =&gt; false } },
    { provide: Router, useValue: routerSpy }
  ]
});
guard = TestBed.inject(AuthGuard);
}); it('should redirect unauthorized users', () => {
expect(guard.canActivate()).toBe(false);
expect(routerSpy.navigate).toHaveBeenCalledWith(&#91;'/login']);
}); });

11. Best Practices for CanActivate Guards

  1. Keep logic simple – Guards should only handle navigation checks.
  2. Combine with services – Centralize authentication and role checks.
  3. Use asynchronous guards for backend validation.
  4. Apply multiple guards for layered security.
  5. Redirect unauthorized users to login or unauthorized pages.
  6. Test guards thoroughly to avoid accidental access.
  7. 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(&#91;'/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(&#91;'/unauthorized']);
return false;
} }

Routes

const routes: Routes = [
  { 
path: 'admin', 
component: AdminComponent, 
canActivate: &#91;AuthGuard, RoleGuard] 
}, { path: 'login', component: LoginComponent }, { path: 'unauthorized', component: UnauthorizedComponent } ];

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *