Monday, September 30, 2024

Angular Access Control CanActivate Route Guard Example

In your Angular application you may need to control access to different parts of your app. To control that kind of authorization and authentication you can use route guards in Angular.

Using route guards, you can perform some action before navigating to another route and you can restrict access to routes based on the outcome of that action. In this post we’ll see example of using CanActivate route guard to restrict access to route based on whether user is authenticated or not.


Why route guards in Angular

Use of route guards to control access in your Angular app may be considered for following scenarios-

  • User is not authorized to navigate to the component rendered by the route user want to navigate to.
  • User is not authenticated (not logged in).
  • You need to fetch some data before you display the target component.
  • You want to save pending changes before leaving a component.
  • You want to ask the user if it's OK to discard pending changes rather than save them.

You can add route guards to the route configuration to handle the above mentioned scenarios.

Value returned by route guard controls the router's behavior:

  • If it returns true, the navigation process continues.
  • If it returns false, the navigation process stops and the user may remain at the same component or navigated to another route.
  • If it returns a UrlTree, the current navigation cancels and a new navigation is initiated to the UrlTree returned.

Route guard interfaces in Angular

In Angular there are multiple guard interfaces:

  1. CanActivate- to control navigation to a route.
  2. CanActivateChild- to control navigation to a child route. Refer Angular CanActivateChild Guard to protect Child Routes to know more about CanActivateChild.
  3. CanDeactivate- to control navigation away from the current route by asking user if it is ok to navigate away. Refer CanDeactivate Guard in Angular With Example to know more about CanDeactivate.
  4. Resolve- to perform route data retrieval before route activation.
  5. CanLoad- to mediate navigation to a feature module loaded asynchronously.

CanActivate interface in Angular

A class implementing CanActivate interface can act as a guard deciding if a route can be activated. If all guards return true, navigation continues. If any guard returns false, navigation is cancelled. If any guard returns a UrlTree, the current navigation is cancelled and a new navigation begins to the UrlTree returned from the guard.

There is one method in CanActivate interface

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree

A routing guard can return an Observable<boolean> or a Promise<boolean> asynchronously or a boolean synchronously.

In Angular 17, CanActivate interface was deprecated and it’s un-deprecated in Angular 18. Now there is one more way to create CanActivate guard using CanActivateFn. In this tutorial we’ll see example of creating CanActivate guard using CanActivate interface as well as using CanActivateFn.

How to use CanActivate routeguard

First thing you need to do is to create a class that implements CanActivate interface. Add that guard to the route which has to be guarded. For example there is a class AuthGuard that implements CanActivate and using that you want to guard the route for account and its child routes.

{path: 'account', canActivate: [AuthGuard], component: AccountsComponent, children: [
 {path: ':acctno', component: AccountComponent}
 ]},

As you can see canActivate property is used for adding guards which takes an array of guards that has to be applied on a specific route.

Authentication using CanActivate routeguard Angular example

In the example we want to ensure that AccountComponent can only be accessed when the user is logged in.

First we’ll create a simple authentication service responsible for authentication and authorization of routes. For simplicity here we’ll check for hardcoded values 'admin' and 'password' as the correct values for user and password rather than fetching the values from persistence.

If value matches, username is also stored into localStorage for further use.

LocalAuthService (localauth.service.ts)

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: 'root'
})
export class LocalAuthService{
  login(user: string, password: string): boolean{
    if(user==='admin' && password==='password'){
      localStorage.setItem('user', user);
      return true;
    }
    return false;   
  }
  logout(){
    localStorage.removeItem('user');
  }

  getUser(): string|null{
    return localStorage.getItem('user');
  }

  isLoggedIn(): boolean{
    return localStorage.getItem('user') !== null;
  }
}

AuthenticateGuardService (authenticate-guard.service.ts)

This is the guard class that implements CanActivate interface. In the canActivate() method you check if the user is already logged in or not. If user is not logged in then navigate to Login page otherwise return true.

import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { LocalAuthService } from "./localauth.service";

@Injectable({
    providedIn: 'root'
})
export class AuthenticateGuardService implements CanActivate{
  constructor(private authService: LocalAuthService, private router: Router){}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const isLoggedIn = this.authService.isLoggedIn();
    console.log("Is user logged in", isLoggedIn);
    if(!isLoggedIn){
      this.router.navigate(['/login']);         
    }
    return true;
  }
}

LoginComponent (login.component.ts)

In the component there is a login() method that calls the login method of the LocalAuthService by passing the entered values of user and password fields. If there is no match then the error message is set otherwise navigate to home page.

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { LocalAuthService } from "../../services/localauth.service";

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})
export class LoginComponent implements OnInit{
  message: string;
  loggedIn = false;
  constructor(private authService: LocalAuthService, private router: Router){
    this.message = '';
    this.loggedIn = this.authService.isLoggedIn();
  }
  
  login(user: string, password: string){
    if(!this.authService.login(user, password)){
      this.message = 'User Name or Password is incorrect';
      this.loggedIn = false;
    }else{
      this.loggedIn = true;
      this.router.navigate(['/home']);
    }
  }
  ngOnInit(): void {
  }
}

login.component.html

<div class="container">
  <div class="d-flex justify-content-center">
    <form *ngIf="!loggedIn">
      <div class="alert alert-danger" role="alert" *ngIf="message">
        {{ message }}
      </div>
      <div class="mb-3">
        <label for="username">User:</label>
        <input class="form-control" name="username" placeholder="Enter user name" #username>
      </div>
    
      <div class="mb-3">
        <label for="password">Password:</label>
        <input class="form-control" type="password" name="password" placeholder="Enter password" #password>
      </div>
      <div class="d-flex justify-content-center mt-3">
        <button type="submit" class="btn btn-primary" (click)="login(username.value, password.value)">
          Login
        </button>
      </div>
    </form>
  </div>
</div>

HomeComponent (home.component.ts)

This is the component for showing home page where you have a login button.

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LocalAuthService } from '../services/localauth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html'
})
export class HomeComponent {
  loggedIn = false;
  user: string = '';
 
  constructor(private authService: LocalAuthService, private router:Router) { 
    this.loggedIn = this.authService.isLoggedIn();
    this.user = this.authService.getUser()||'';
  }

  onClickLogin(){
    this.router.navigate(['/login'])
  }
    
  logout(){
    this.authService.logout();
  }

}

home.component.html

<h4>Welcome to XYZ Bank</h4>
<div *ngIf="!loggedIn">
  <button class="btn btn-primary" (click)="onClickLogin()">Login</button>
</div>
<div *ngIf="loggedIn">
  Logged in as <b>{{ user }}</b>
  <a href (click)="logout()"> Log out</a>
</div>

AccountsComponent (accounts.component.ts)

This is the parent component which shows the account numbers.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-accounts',
  templateUrl: './accounts.component.html'
})
export class AccountsComponent implements OnInit{
  accountnumbers:number[] = [];
  constructor(private router: Router, private route: ActivatedRoute) {}
  ngOnInit() {
    this.accountnumbers = [1001, 1002];
  }
}

accounts.component.html

<div class= "row">
  <div class="col-sm-6">
    <h2>Account Numbers</h2>
    <div class="list-group">
      <a [routerLink]="['/account', accountnumber]"    
       href="#"      
       class="list-group-item"   
       *ngFor="let accountnumber of accountnumbers">
          {{ accountnumber }}
      </a>
    </div>
    <br>
  </div>
  <div class="col-sm-6">
    <router-outlet></router-outlet>
  </div>
</div>

AccountDetailComponent (account-detail.component.ts)

This is the child component that shows the details for the selected account number.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';

@Component({
  selector: 'app-account',
  templateUrl: './account-detail.component.html'
})
export class AccountDetailComponent implements OnInit{
  acctNo!: string;
  a: any;
  account: {accountnumber: number, type: string, balance: number}= {accountnumber: 9999, type: 'C', balance:0};
  constructor(private route: ActivatedRoute, private router: Router){ }
  accountDetails = [
    {
      accountnumber: 1001,
      type: 'Saving', 
      balance: 22000
    },
    {
      accountnumber: 1002,
      type: 'Checking',
      balance: 1000
    }
  ];

  ngOnInit() {
   
    this.route.params.subscribe((params: Params)=> {
      this.acctNo = params['acctno'];
      //console.log(typeof(this.acctNo));
      let acctno =  Number(this.acctNo);
      console.log('Account ', acctno);
      //console.log('Details ', this.accountDetails);
      this.a = this.accountDetails.find((e) => e.accountnumber === acctno);
      //console.log('Value of a ',  this.account);
      console.log('Value of a ', this.a);
      if(this.a !== null && this.a !== undefined){
        this.account = this.a;
      }
    });      
  }
}

account.component.html

<h2>Account Details</h2>
<div class="row">
  <div class="col-sm-6">
    <label>Account Number: </label> {{ account.accountnumber }}
  </div>
</div>
<div class="row">
  <div class="col-sm-6">
    <label>Account Type: </label> {{ account.type }}
  </div>
</div>
<div class="row">
  <div class="col-sm-6">
    <label>Balance: </label> {{account.balance}}
  </div>
</div>

app-routing.module.ts

This is the routing module where we have the route definitions. With AccountsComponent route there is a canActivate guard class specified. This route guard will work on the child route AccountComponent too.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './RoutingDemos/home.component';
import { LoginComponent } from './nestedroutedemo/login/login.component';
import { AuthenticateGuardService } from './services/authenticate-guard.service';
import { AccountDetailComponent } from './nestedroutedemo/accounts/account/account-detail.component';
import { AccountsComponent } from './nestedroutedemo/accounts/accounts.component';

const routes: Routes = [
  {path: 'login', component: LoginComponent}, 
  {path: 'home', component: HomeComponent},                  
  {path: 'account', canActivate: [AuthenticateGuardService], component: AccountsComponent, 
    children: [      
      {path: ':acctno', component: AccountDetailComponent}     
  ]},
  {path: '', redirectTo:'/home', pathMatch: 'full'},
];

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

routemenu.component.ts

This is the menu component.

 
import { OnInit, Component } from '@angular/core';
import { LocalAuthService } from '../services/localauth.service';

@Component({
  selector: 'app-routemenu',
  templateUrl: './routemenu.component.html'
})
export class RouteMenuComponent {
  constructor(private authService: LocalAuthService){ }

  getUserName(){
    return this.authService.getUser();
  }
  onLogOut(){
    this.authService.logout();
  }

 loggedIn(){
    return this.authService.isLoggedIn()
  }
}

routemenu.component.html

<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<div class="container-fluid">
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsibleNavbar" aria-controls="collapsibleNavbar" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="collapsibleNavbar">
    <ul class="nav navbar-nav">
      <li class="nav-item" routerLinkActive="active"><a class="nav-link" [routerLink]="['/home']">Home</a></li>
      <li class="nav-item" routerLinkActive="active"><a class="nav-link" [routerLink]="['/account']">Accounts</a></li>
      <!-- <li class="nav-item" routerLinkActive="active"><a class="nav-link" [routerLink]="['/service']">Services</a></li> -->
    </ul>
  </div>
  <ul class="navbar-nav ml-auto">

    <li class="nav-item">
      <a class="nav-link" [routerLink]="['/login']" *ngIf="!loggedIn()">Login</a>
    </li>
    <li class="nav-item">
      <a href class="nav-link" *ngIf="loggedIn()">{{ getUserName() }}</a>
    </li>
    <li class="nav-item">
      <a href class="nav-link" (click)="onLogOut()" *ngIf="loggedIn()">LogOut</a>
    </li>
  </ul>
</div>

</nav>
<div class="container">
  <div class="row"><p></p></div>
  <div class="row">
    <div class="col-sm-12">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

In the app.component.html add the selector for the above mentioned menu.

app.component.html

  <app-routemenu></app-routemenu>

All the components (in decarations) and services (in providers) are to be added to the app module too.

imports: [
  BrowserModule,
  AppRoutingModule
],

Using CanActivateFn in Angular

You can also use a function as a canActivate guard on a Route. For the same setup as already explained if you want to have a guard to verify whether user is logged in or not written as a function rather than as an implementation of CanActivate interface then you can use the following code.

auth-guard.ts

import { inject } from "@angular/core";
import { LocalAuthService } from "../../services/localauth.service";
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from "@angular/router";

export const authGuard : CanActivateFn = (
    route: ActivatedRouteSnapshot, 
    state: RouterStateSnapshot
  ) => {
    const authService = inject(LocalAuthService);
    const router = inject(Router);
  
    if (authService.isLoggedIn()) {
      return true;
    }
  
    // Redirect to the login page
    return router.parseUrl('/login');
};
  

You can use this guard function as value for the canActivate attribute in route configuration.

  const routes: Routes = [
  {path: 'login', component: LoginComponent}, 
  {path: 'home', component: HomeComponent},                  
  {path: 'account', canActivate: [authGuard], component: AccountsComponent, 
    children: [      
      {path: ':acctno', component: AccountDetailComponent}     
  ]},
  {path: '', redirectTo:'/home', pathMatch: 'full'},
];

Once the configuration is done and code is successfully compiled access http://localhost:4200

route guard in Angular

If you try to access Account menu option without logging in you won’t be able to access it because of the canActivate() method implementation. You will be redirected to the login page.

Login Page

Angular CanActivate example

After successful login.

canActivate route guard

After logging in, Accounts menu option is accessible.

CanActivate Route Guard

That's all for this topic Angular Access Control CanActivate Route Guard. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Angular Tutorial Page


Related Topics

  1. Angular Route Parameters - Setting and Fetching
  2. Passing Query Parameters in Angular Routing
  3. Setting Wild Card Route in Angular
  4. Nested Route (Child Route) in Angular
  5. Highlight Currently Selected Menu Item Angular Routing Example

You may also like-

  1. Angular Project Structure With File Description
  2. Service in Angular With Examples
  3. Angular Custom Event Binding Using @Output Decorator
  4. Angular Class Binding With Examples
  5. Lambda Expression Examples in Java
  6. Object class in Java
  7. Bubble Sort Program in Python
  8. Spring Object XML Mapping (OXM) JAXB Example

No comments:

Post a Comment