116

I have an AuthGuard (used for routing) that implements CanActivate.

canActivate() {
    return this.loginService.isLoggedIn();
}

My problem is, that the CanActivate-result depends on a http-get-result - the LoginService returns an Observable.

isLoggedIn():Observable<boolean> {
    return this.http.get(ApiResources.LOGON).map(response => response.ok);
}

How can i bring those together - make CanActivate depend on a backend state?

# # # # # #

EDIT: Please note, that this question is from 2016 - a very early stage of angular/router has been used.

# # # # # #

Philipp
  • 4,180
  • 7
  • 27
  • 41
  • 2
    Have you read here? https://angular.io/docs/ts/latest/guide/router.html search for Route Guards Here is api reference for CanActivate: https://angular.io/docs/ts/latest/api/router/index/CanActivate-interface.html as you see it can return either boolean or Observable – mollwe Jun 22 '16 at 02:04
  • 4
    `canActivate()` can return an `Observable`, just make sure that the `Observable` has completed (ie. `observer.complete()`). – Philip Bulley Jul 14 '16 at 13:31
  • 1
    @PhilipBulley what if the observable emits more values and then completes? What does the guard do? What I have seen so far is use of `take(1)` Rx operator to achieve the completnes of stream, What if I forget to add it? – Felix Nov 09 '17 at 14:16

7 Answers7

167

You should upgrade "@angular/router" to the latest . e.g."3.0.0-alpha.8"

modify AuthGuard.ts

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

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.loginService
            .isLoggedIn()
            .map((e) => {
                if (e) {
                    return true;
                }
            })
            .catch(() => {
                this.router.navigate(['/login']);
                return Observable.of(false);
            });
    }
}

If you have any questions, ask me!

Matias de Andrea
  • 194
  • 2
  • 11
Kery Hu
  • 5,626
  • 11
  • 34
  • 51
  • 3
    It's worth pointing out that this works with promises in a very similar way. For my implementation, assuming `isLoggedIn()` is a `Promise`, you can do `isLoggedIn().then((e) => { if (e) { return true; } }).catch(() => { return false; });` Hopefully this helps future travelers! – kbpontius Jan 26 '17 at 20:32
  • 7
    I had to add `import 'rxjs/add/observable/of';` – marc_aragones Mar 07 '17 at 12:25
  • 1
    not a good answer now IMO .. it provides no detail of what is happening server side ... it is out of date at alpha! ... it does not follow this best practice .. https://angular.io/docs/ts/latest/guide/server-communication.html#!#do-not-return-the-response-object .. see my updated answer below (hopefully one day above) – danday74 Mar 09 '17 at 18:15
  • 1
    canActivate should retun Observable – Yoav Schniederman Mar 28 '17 at 11:08
  • canActivate() also accepts return type of Observable so we can directly return as observable – Parvesh kumar May 07 '18 at 10:23
  • 1
    Now it is `import { Observable, of } from 'rxjs';` – aarjithn Oct 08 '18 at 10:11
  • Care to explain what you did? – Pratik Joshi Aug 08 '19 at 03:13
  • 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 03:16
91

Updating Kery Hu's answer for Angular 5+ and RxJS 5.5 where the catch operator is deprecated. You should now use the catchError operator in conjunction with pipe and lettable operators.

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { catchError, map} from 'rxjs/operators';
import { of } from 'rxjs/observable/of';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private loginService: LoginService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>  {
    return this.loginService.isLoggedIn().pipe(
      map(e => {
        if (e) {
          return true;
        } else {
          ...
        }
      }),
      catchError((err) => {
        this.router.navigate(['/login']);
        return of(false);
      })
    );
  }   
  
}
Derek Hill
  • 5,965
  • 5
  • 55
  • 74
  • 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 pipe() if I have another ajax too. – Pratik Joshi Aug 08 '19 at 03:16
  • 1
    very useful also for angular 10 – Ruslan López Nov 23 '20 at 10:03
14

canActivate() accepts Observable<boolean> as returned value. The guard will wait for the Observable to resolve and look at the value. If 'true' it will pass the check, else ( any other data or thrown error ) will reject the route.

You can use the .map operator to transform the Observable<Response> to Observable<boolean> like so:

canActivate(){
    return this.http.login().map((res: Response)=>{
       if ( res.status === 200 ) return true;
       return false;
    });
}
CodyBugstein
  • 21,984
  • 61
  • 207
  • 363
mp3por
  • 1,796
  • 3
  • 23
  • 35
3

I've done it in this way:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.userService.auth(() => this.router.navigate(['/user/sign-in']));}

As you can see I'm sending a fallback function to userService.auth what to do if http call fails.

And in userService I have:

import 'rxjs/add/observable/of';

auth(fallback): Observable<boolean> {
return this.http.get(environment.API_URL + '/user/profile', { withCredentials: true })
  .map(() => true).catch(() => {
    fallback();
    return Observable.of(false);
  });}
danday74
  • 52,471
  • 49
  • 232
  • 283
  • really good answer in terms of the .map() function - really really good :) - did not use the fallback callback - instead i subscribed to the auth Observable in the canActivate method - thanks very much for the idea – danday74 Mar 09 '17 at 17:21
3

This may help you

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Select } from '@ngxs/store';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AuthState } from 'src/app/shared/state';

export const ROLE_SUPER = 'ROLE_SUPER';

@Injectable()
export class AdminGuard implements CanActivate {

    @Select(AuthState.userRole)
    private userRoles$: Observable<string[]>;

    constructor(private router: Router) {}

    /**
     * @description Checks the user role and navigate based on it
     */

    canActivate(): Observable<boolean> {
    return this.userRoles$.pipe(
        take(1),
        map(userRole => {
        console.log(userRole);
        if (!userRole) {
            return false;
        }
        if (userRole.indexOf(ROLE_SUPER) > -1) {
            return true;
        } else {
            this.router.navigate(['/login']);
        }
        return false;
        })
    );
    } // canActivate()
} // class
rofrol
  • 14,438
  • 7
  • 79
  • 77
Kalanka
  • 1,019
  • 2
  • 18
  • 31
-1

CanActivate does work with Observable but fails when 2 calls are made like CanActivate:[Guard1, Guard2].
Here if you return an Observable of false from Guard1 then too it will check in Guard2 and allow access to route if Guard2 returns true. In order to avoid that, Guard1 should return a boolean instead of Observable of boolean.

Arjunsingh
  • 703
  • 9
  • 22
-10

in canActivate(), you can return a local boolean property (default to false in your case).

private _canActivate: boolean = false;
canActivate() {
  return this._canActivate;
}

And then in the result of the LoginService, you can modify the value of that property.

//...
this.loginService.login().subscribe(success => this._canActivate = true);