112

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?

1
  • import Observable like this -> import { Observable } from 'rxjs/Observable'; Commented Feb 23, 2018 at 22:18

9 Answers 9

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

Sign up to request clarification or add additional context in comments.

7 Comments

can you show how to use this class in other components?
Not sure what you mean. You use this with routes, not components. See angular.io/docs/ts/latest/guide/router.html#!#guards
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 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 yes, thanks for that. I didn't realise I was subscribing to a subscriber. Thanks a lot for the answer! It works great
|
43

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);
      });
    }
  }
}

2 Comments

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
Actually, this is the right answer!!! A good way to use the canActivate method calling an async function.
28

canActivate can return a Promise that resolves a boolean too

Comments

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);
      }
    }

     })
   })
  }
}

2 Comments

Thank you. This solution waits until the api call respond and then redirect.. perfect.
This looks like an example of the explicit Promise constructor antipattern (stackoverflow.com/questions/23803743/…). 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.
8

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;
        }
      })
    );
  }
}

1 Comment

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.
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.

Comments

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);
  }
}

Comments

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!')
      })
    );
  }

Comments

2

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 } } },

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.