21

I'm trying to build an AuthGuard for Angular 2 routes using Firebase Auth.

This is the AuthGuard Service:

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

@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    if (this.AuthService.loggedIn) { return true; }

    this.router.navigate(['login']);
    return false;
  }
}

And this is the AuthService which checks if the user's logged in and binds the result to the property 'loggedIn' in its constructor.

import { Injectable } from '@angular/core';
import { AngularFire } from 'angularfire2';
import { Router } from '@angular/router';

@Injectable()
export class AuthService {
loggedIn: boolean = false;

  constructor(
    public af: AngularFire,
    public router: Router) {
        af.auth.subscribe(user => {
            if(user){
                this.loggedIn = true;
            }
        });
    }
}

The problem here is obviously asynchrony. AuthGuard's canActivate() always returns a false value because the subscription doesn't receive the data in time to change 'loggedIn' to true.

What's the best practice to fix this?

EDIT:

Changed the AuthGuard to return an observable.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.af.auth.map((auth) => {
        if (!auth) {
          this.router.navigateByUrl('login');
          return false;
        }
        return true;
    });
  }

It kind of works since you don't get redirected to login... But the target AuthGuarded Component is not rendered.

Not sure if it has to do with my app.routes. This is the code for that part:

const routes: Routes = [
  { path: '', component: MainComponent, canActivate: [AuthGuard] },
  ...
];

export const routing = RouterModule.forRoot(routes);
cerealex
  • 1,649
  • 4
  • 17
  • 37

3 Answers3

11

Complete the observable using .take(1) to make the component render.

import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.af.auth.map((auth) => {
        if (!auth) {
          this.router.navigateByUrl('login');
          return false;
        }
        return true;
    }).take(1);
  }
codedesignr
  • 176
  • 1
  • 5
  • It seems after upgrading to Angular 2.0.0 I can no longer use map(). Ts says `Property 'map' does not exist in type 'AngularFireAuth'`. – cerealex Sep 19 '16 at 09:02
  • I had to add import 'rxjs/add/operator/map' and import 'rxjs/add/operator/take' to make it work, but it works after all. Thanks! – cerealex Sep 19 '16 at 16:12
  • Glad it worked for you. I should have mentioned the imports, as I had the same issue after upgrading from RC5. – codedesignr Sep 19 '16 at 19:02
  • Doesn't work for me. I keep getting the error: `error TS2322: Type 'Observable' is not assignable to type 'boolean'.` My problem is here: http://stackoverflow.com/questions/39687114/canactivate-doesnt-respond-upon-subscription-change-angular-2-router-angularfir – KhoPhi Sep 26 '16 at 14:40
  • I added the required `rxjs` related import statements – codedesignr Oct 01 '16 at 20:22
10

For newer versions of AngularFire, you have to use authState instead:

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

  canActivate(): Observable<boolean> {
    return this.afAuth.authState
      .take(1)
      .map(authState => !!authState)
      .do(auth => !auth ? this.router.navigate(['/login']) : true);
  }
}

Don't forget to import take, map and do from rxjs/add/operator.

Will
  • 2,523
  • 1
  • 13
  • 21
0

Have tested this one with Angular 11 and angular fire 6

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.afAuth.authState.pipe(
  map(authState => !!authState),
  map(auth => {
    if (!auth) {
      this.router.navigate(['/sign-in']);
    }
    return auth;
  }),
 );
}
Mohan Dere
  • 4,497
  • 1
  • 25
  • 21