107

I am trying to use Angular2 router guards to restrict access to some pages in my app. I am using Firebase Authentication. In order to check if a user is logged in with Firebase, I have to call .subscribe() on the FirebaseAuth object with a callback. This is the code for the guard:

import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from "angularfire2/angularfire2";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        this.auth.subscribe((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        });
    }
}

When a navigate to a page that has the guard on it, either authenticated, or not authenticated is printed to the console (after some delay waiting for the response from firebase). However, the navigation is never completed. Also, if I am not logged in I am redirected to the /login route. So, the issue I am having is return true doesn't display the requested page to the user. I'm assuming this is because I am using a callback, but I am unable to figure out how to do it otherwise. Any thoughts?

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Evan Salter
  • 1,169
  • 2
  • 7
  • 11

9 Answers9

145

canActivate needs to return an Observable that completes:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        return this.auth.map((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        }).first(); // this might not be necessary - ensure `first` is imported if you use it
    }
}

There is a return missing and I use map() instead of subscribe() because subscribe() returns a Subscription not an Observable

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • can you show how to use this class in other components? –  Jul 18 '16 at 05:00
  • Not sure what you mean. You use this with routes, not components. See https://angular.io/docs/ts/latest/guide/router.html#!#guards – Günter Zöchbauer Jul 18 '16 at 05:02
  • The Observable doesn't run in my case. I don't see any console output. However, if I return booleans conditionally (as in the docs) console gets logged. Is this.auth a simple Observable? – cortopy Aug 16 '16 at 18:12
  • @cortopy `auth` is a value emitted by the observable (might be just `true` or `false`). The observable is executed when the router subscribes to it. Maybe something is missing in your configuration. – Günter Zöchbauer Aug 16 '16 at 18:18
  • 1
    @günter-zöchbauer yes, thanks for that. I didn't realise I was subscribing to a subscriber. Thanks a lot for the answer! It works great – cortopy Aug 17 '16 at 22:53
  • Did you manage to write unit test for that? For simple canActivate guard is easy, but with async call gets a bit tricky – DAG Aug 04 '17 at 10:14
  • your info on subscriber vs observable immediately solved the last several hours that have been driving me nuts. Thanks! – Briana Finney Jan 23 '20 at 23:29
42

You might use Observable to handle the async logic part. Here is the code I test for example:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { DetailService } from './detail.service';

@Injectable()
export class DetailGuard implements CanActivate {

  constructor(
    private detailService: DetailService
  ) {}

  public canActivate(): boolean|Observable<boolean> {
    if (this.detailService.tempData) {
      return true;
    } else {
      console.log('loading...');
      return new Observable<boolean>((observer) => {
        setTimeout(() => {
          console.log('done!');
          this.detailService.tempData = [1, 2, 3];
          observer.next(true);
          observer.complete();
        }, 1000 * 5);
      });
    }
  }
}
Luozt
  • 569
  • 4
  • 8
  • 2
    That is actually a good answer which really helped me. Even though I had a similar question but the accepted answer didn't resolve my issue. This one did – Konstantin Jun 12 '19 at 12:49
  • Actually, this is the right answer!!! A good way to use the canActivate method calling an async function. – danilo Jul 03 '19 at 18:58
25

canActivate can return a Promise that resolves a boolean too

paulsouche
  • 251
  • 3
  • 3
22

You can return true|false as a promise.

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from "../services/authorization.service";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private router: Router, private authService:AuthService) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  return new Promise((resolve, reject) => {
  this.authService.getAccessRights().then((response) => {
    let result = <any>response;
    let url = state.url.substr(1,state.url.length);
    if(url == 'getDepartment'){
      if(result.getDepartment){
        resolve(true);
      } else {
        this.router.navigate(['login']);
        resolve(false);
      }
    }

     })
   })
  }
}
rogue lad
  • 2,413
  • 2
  • 29
  • 32
  • 1
    That new Promise object saves me :D Thanks. – canmustu Feb 21 '19 at 00:30
  • 1
    Thank you. This solution waits until the api call respond and then redirect.. perfect. – Philip Enc Mar 31 '20 at 09:57
  • 1
    This looks like an example of the explicit Promise constructor antipattern (https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it). The code example suggests getAccessRights() returns a Promise already, so I'd try directly returning it with `return this.authService.getAccessRights().then...` and return the boolean result without wrapping in `resolve`. – rob3c Dec 17 '20 at 02:48
7

In the most recent version of AngularFire, the following code works (related to the best answer). Note the usage of "pipe" method.

import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {AngularFireAuth} from '@angular/fire/auth';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';

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

  constructor(private afAuth: AngularFireAuth, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.afAuth.authState.pipe(
      map(user => {
        if(user) {
          return true;
        } else {
          this.router.navigate(['/login']);
          return false;
        }
      })
    );
  }
}
Ajitesh
  • 956
  • 10
  • 14
  • I have 1 more XHR call after isLoggedIn() , and result of XHR is used in 2nd XHR call. How to have 2nd ajax call which will accept for 1st result? The example you gave is pretty easy, can you pls let me know how to use map if I have another ajax too. – Pratik Joshi Aug 08 '19 at 04:00
6

To expand on the most popular answer. The Auth API for AngularFire2 has changes somewhat. This is new signature to achieve a AngularFire2 AuthGuard:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuardService implements CanActivate {

  constructor(
    private auth: AngularFireAuth,
    private router : Router
  ) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean>|boolean {
    return this.auth.authState.map(User => {
      return (User) ? true : false;
    });
  }
}

Note: This is a fairly naive test. You can console log the User instance to see if you would like to test against some more detailed aspect of the user. But should at least help protect routes against user's who are not logged in.

Rudi Strydom
  • 4,417
  • 5
  • 21
  • 30
5

In order to show another way of implementation. As per documentation, and mentioned by other answers return type of CanActivate can also be a Promise that resolves to boolean.

Note: The example shown is implemented in Angular 11, but is applicable to Angular 2+ versions.

Example:

import {
  Injectable
} from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanActivateChild,
  Router,
  RouterStateSnapshot,
  UrlTree
} from '@angular/router';
import {
  Observable
} from 'rxjs/Observable';
import {
  AuthService
} from './auth.service';

@Injectable()
export class AuthGuardService implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.checkAuthentication();
  }

  async checkAuthentication(): Promise < boolean > {
    // Implement your authentication in authService
    const isAuthenticate: boolean = await this.authService.isAuthenticated();
    return isAuthenticate;
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.canActivate(childRoute, state);
  }
}
rishabh sethi
  • 105
  • 1
  • 7
3

In my case I needed to handle different behavior depends on the response status error. This is how it works for me with RxJS 6+:

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private auth: AngularFireAuth, private router: Router) {}

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      tap({
        next: val => {
          if (val) {
            console.log(val, 'authenticated');
            return of(true); // or if you want Observable replace true with of(true)
          }
          console.log(val, 'acces denied!');
          return of(false); // or if you want Observable replace true with of(true)
        },
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
}

In some cases this might not work, at least the next part of tap operator. Remove it and add old good map like below:

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      map((auth) => {
        if (auth) {
          console.log('authenticated');
          return true;
        }
        console.log('not authenticated');
        this.router.navigateByUrl('/login');
        return false;
      }),
      tap({
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
mpro
  • 14,302
  • 5
  • 28
  • 43
1

using async await... you wait for the promise to resolve

async getCurrentSemester() {
    let boolReturn: boolean = false
    let semester = await this.semesterService.getCurrentSemester().toPromise();
    try {

      if (semester['statusCode'] == 200) {
        boolReturn = true
      } else {
        this.router.navigate(["/error-page"]);
        boolReturn = false
      }
    }
    catch (error) {
      boolReturn = false
      this.router.navigate(["/error-page"]);
    }
    return boolReturn
  }

Here is my auth gaurd (@angular v7.2)

async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    let security: any = null
    if (next.data) {
      security = next.data.security
    }
    let bool1 = false;
    let bool2 = false;
    let bool3 = true;

    if (this.webService.getCookie('token') != null && this.webService.getCookie('token') != '') {
      bool1 = true
    }
    else {
      this.webService.setSession("currentUrl", state.url.split('?')[0]);
      this.webService.setSession("applicationId", state.root.queryParams['applicationId']);
      this.webService.setSession("token", state.root.queryParams['token']);
      this.router.navigate(["/initializing"]);
      bool1 = false
    }
    bool2 = this.getRolesSecurity(next)
    if (security && security.semester) {
      // ----  watch this peace of code
      bool3 = await this.getCurrentSemester()
    }

    console.log('bool3: ', bool3);

    return bool1 && bool2 && bool3
  }

route is

    { path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: {  security: { semester: true } } },
Mohsin Ejaz
  • 316
  • 3
  • 7