Skip to main content

Angular Route Guards

Introduction

Route Guards are a powerful feature in Angular's routing system that allow you to control access to specific routes in your application. They act as gatekeepers that can prevent unauthorized users from accessing certain parts of your application, redirect users to login pages, prevent users from leaving a page with unsaved changes, and much more.

In this tutorial, we'll explore different types of Angular Route Guards, understand how they work, and implement them in real-world scenarios to secure our application routes.

What are Route Guards?

Route Guards are interfaces provided by the Angular Router that allow you to control the accessibility of routes based on conditions you define. Before a route is activated or deactivated, Angular checks if all the guards return true. If any guard returns false, the navigation is cancelled.

Angular provides several built-in guard interfaces:

  1. CanActivate: Controls if a route can be activated
  2. CanActivateChild: Controls if children of a route can be activated
  3. CanDeactivate: Controls if a user can leave a route
  4. Resolve: Performs route data retrieval before route activation
  5. CanLoad: Controls if a module can be loaded lazily

Creating Your First Route Guard

Let's start by creating a simple CanActivate guard that checks if a user is logged in before allowing access to a route.

First, we'll generate a new guard using the Angular CLI:

bash
ng generate guard auth/auth

When prompted, select CanActivate as the interface you want to implement. The CLI will create a new guard file for you.

Now, let's implement our auth guard:

typescript
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
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(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isLoggedIn()) {
return true;
} else {
// Redirect to the login page
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}
}

In this example, we're using a hypothetical AuthService that has an isLoggedIn() method to check if the user is authenticated. If the user is not logged in, we redirect them to the login page and pass the current URL as a query parameter, so we can redirect them back after successful login.

Using the Route Guard in Routes

Once we have created our guard, we need to apply it to our routes. Let's see how to do that:

typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { LoginComponent } from './login/login.component';
import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard] // Apply our guard here
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthGuard] // Protect the entire admin module
}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

Now, the /profile route and the /admin module are protected by our AuthGuard. Users will only be able to access these routes if they are logged in.

Implementing Different Types of Guards

CanActivateChild Guard

The CanActivateChild guard is similar to the CanActivate guard, but it controls access to child routes. Let's extend our AuthGuard to implement this interface:

typescript
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
CanActivateChild,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {

constructor(
private authService: AuthService,
private router: Router
) {}

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.checkLogin(state.url);
}

canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.canActivate(childRoute, state);
}

private checkLogin(url: string): boolean {
if (this.authService.isLoggedIn()) {
return true;
}

// Store the attempted URL for redirecting
this.authService.redirectUrl = url;

// Navigate to the login page
this.router.navigate(['/login']);
return false;
}
}

Now we can use this guard to protect child routes:

typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivateChild: [AuthGuard],
children: [
{ path: 'dashboard', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: AdminSettingsComponent }
]
}
];

CanDeactivate Guard

The CanDeactivate guard is useful when you want to prevent users from accidentally leaving a page with unsaved changes. Let's create a new guard for this purpose:

typescript
// can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {

canDeactivate(
component: CanComponentDeactivate
): Observable<boolean> | Promise<boolean> | boolean {
return component.canDeactivate ? component.canDeactivate() : true;
}
}

Now, let's implement this interface in a component:

typescript
// edit-form.component.ts
import { Component } from '@angular/core';
import { CanComponentDeactivate } from './can-deactivate.guard';

@Component({
selector: 'app-edit-form',
template: `
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<button (click)="save()">Save</button>
</form>
`
})
export class EditFormComponent implements CanComponentDeactivate {
form: FormGroup;
saved = false;

constructor(private fb: FormBuilder) {
this.form = this.fb.group({
name: ['']
});

// Track form changes
this.form.valueChanges.subscribe(() => {
this.saved = false;
});
}

save() {
// Save the form data
this.saved = true;
}

canDeactivate(): boolean {
if (this.form.dirty && !this.saved) {
return confirm('You have unsaved changes! Do you really want to leave?');
}
return true;
}
}

And finally, add the guard to our route:

typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'edit',
component: EditFormComponent,
canDeactivate: [CanDeactivateGuard]
}
];

Resolve Guard

The Resolve guard is used to fetch data before a route is activated. This is useful when you want to ensure that certain data is available before the component is instantiated.

First, let's create a resolver:

typescript
// user-resolver.service.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { UserService } from './user.service';
import { User } from './user.model';

@Injectable({
providedIn: 'root'
})
export class UserResolver implements Resolve<User> {

constructor(private userService: UserService) {}

resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<User> {
const userId = route.paramMap.get('id');

return this.userService.getUser(userId).pipe(
catchError(error => {
console.error('Error retrieving user:', error);
return of(null); // Return a default value or redirect
})
);
}
}

Now, let's use this resolver in our routes:

typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'user/:id',
component: UserDetailComponent,
resolve: {
user: UserResolver
}
}
];

And in our component, we can access the resolved data:

typescript
// user-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { User } from './user.model';

@Component({
selector: 'app-user-detail',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserDetailComponent implements OnInit {
user: User;

constructor(private route: ActivatedRoute) {}

ngOnInit() {
// The resolved user is available in the route's data
this.user = this.route.snapshot.data['user'];
}
}

CanLoad Guard

The CanLoad guard controls whether a module can be lazy loaded. This is useful for preventing unauthorized users from downloading modules they don't have permission to access.

typescript
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
CanLoad,
Route,
UrlSegment,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanLoad {

constructor(
private authService: AuthService,
private router: Router
) {}

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.checkLogin(state.url);
}

canLoad(
route: Route,
segments: UrlSegment[]
): boolean {
const url = `/${segments.map(s => s.path).join('/')}`;
return this.checkLogin(url);
}

private checkLogin(url: string): boolean {
if (this.authService.isLoggedIn()) {
return true;
}

// Store the attempted URL for redirecting
this.authService.redirectUrl = url;

// Navigate to the login page
this.router.navigate(['/login']);
return false;
}
}

Now, let's use this guard to protect a lazy-loaded module:

typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
}
];

Real-World Application: Role-Based Route Protection

Let's create a more sophisticated route guard that checks not just if a user is logged in, but also if they have the required role to access a route:

typescript
// role.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {

constructor(
private authService: AuthService,
private router: Router
) {}

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// Get the required roles from the route data
const requiredRoles = route.data['roles'] as Array<string>;

// Check if the user has the required roles
if (this.authService.isLoggedIn() &&
this.checkRoles(requiredRoles)) {
return true;
}

// User doesn't have required roles, redirect to access denied page
if (this.authService.isLoggedIn()) {
this.router.navigate(['/access-denied']);
} else {
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
}

return false;
}

private checkRoles(requiredRoles: string[]): boolean {
// If no roles are required, allow access
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}

// Check if the user has any of the required roles
const userRoles = this.authService.getCurrentUserRoles();
return requiredRoles.some(role => userRoles.includes(role));
}
}

Now, let's use this guard with route data:

typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [RoleGuard],
data: { roles: ['ADMIN'] }
},
{
path: 'manager',
component: ManagerComponent,
canActivate: [RoleGuard],
data: { roles: ['MANAGER', 'ADMIN'] }
},
{
path: 'user-dashboard',
component: UserDashboardComponent,
canActivate: [RoleGuard],
data: { roles: ['USER', 'MANAGER', 'ADMIN'] }
},
{
path: 'access-denied',
component: AccessDeniedComponent
}
];

In this example, the admin route requires the 'ADMIN' role, the manager route requires either the 'MANAGER' or 'ADMIN' role, and the user-dashboard route is accessible to users with any of the three roles.

Summary

Angular Route Guards are powerful tools for controlling navigation in your application. They allow you to:

  • Protect routes from unauthorized access with CanActivate and CanActivateChild
  • Prevent users from leaving a page with unsaved changes using CanDeactivate
  • Preload data before navigating to a route with Resolve
  • Control lazy loading of modules with CanLoad

By using Route Guards effectively, you can create a more secure and user-friendly application that directs users appropriately based on their authentication status, permissions, and actions.

Additional Resources

Exercises

  1. Create a new Angular application with a login page and at least three protected routes.
  2. Implement an AuthGuard that checks if a user is logged in.
  3. Implement a RoleGuard that restricts access based on user roles.
  4. Create a form component with a CanDeactivate guard that warns users about unsaved changes.
  5. Implement a Resolve guard to prefetch data for a user profile page.

By completing these exercises, you will gain a solid understanding of how to implement and use Angular Route Guards in real-world scenarios.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)