Angular AuthGuard – Checking Authentication and Database Entry Sequentially

Issue

To allow access to the admin route I have to check two things:

  1. If an user is authenticated
  2. If this user is an admin. I get the admin state from the firebase database.

AdminGuard

import {ActivatedRouteSnapshot, CanActivate, Router, 
RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {AngularFireAuth} from 'angularfire2/auth';
import {Injectable} from '@angular/core';
import {DbActionsService} from '../services/db-actions.service';
import {AuthService} from '../services/auth.service';
import 'rxjs/add/operator/map';

@Injectable()
export class AdminGuard implements CanActivate {

  constructor(private afAuth: AngularFireAuth, private router: Router, private dbAction: DbActionsService, private authService : AuthService) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    if (this.afAuth.auth.currentUser) {
      return this.dbAction.getUserProfileData(this.afAuth.auth.currentUser.email).map((user) => {
        if (user[0].admin) {
          return true;
        } else {
          this.router.navigate(['/']);
          return false;
        }
      }).take(1)
    } else {
      this.router.navigate(['/']);
    }
  }
}

Service Function

getUserProfileData(userEmail: string) {
    this.dataRef = this.afDatabase.list('data/users', ref => ref.orderByChild('profile/email').equalTo(userEmail));
    this.data = this.dataRef.snapshotChanges().map(changes => {
      return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
    });
    return this.data;
  }

This works fine, however, my main problem is when I refresh (or initially load) the page on that has AdminGuard it always redirects me to the home page since the AdminGuard doesn’t wait for the authentication response.

What I tried

A new AuthService

import { Injectable } from '@angular/core';
import {AngularFireAuth} from 'angularfire2/auth';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class AuthService {
  private user: any;

  constructor(private afAuth: AngularFireAuth) { }

  setUser(user) {
    this.user = user;
  }
  getAuthenticated(): Observable<any> {
    return this.afAuth.authState;
  }

}

New AdminGuard

import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {AngularFireAuth} from 'angularfire2/auth';
import {Injectable} from '@angular/core';
import {DbActionsService} from '../services/db-actions.service';
import {AuthService} from '../services/auth.service';
import 'rxjs/add/operator/map';

@Injectable()
export class AdminGuard implements CanActivate {

  constructor(private afAuth: AngularFireAuth, private router: Router, private dbAction: DbActionsService, private authService : AuthService) {}

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

    return this.authService.getAuthenticated().map(user => {
      this.authService.setUser(user);
      return user ? true : false;
    });

  }
}

This works finde with Auth Check on initial load, but how I implement also the database conditional to check if the user is an admin? I have no idea…

Solution

Here’s a guard that should work based on your code, let’s break it down:

  1. Get the user authState observable.
  2. Pipe in switchMap to switch to the database array observable you want.
  3. Map the admin property to boolean with a double bang.
  4. Use tap to handle the redirect.
import { tap, map, switchMap, take } from 'rxjs/operators;

// ...omitted

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

  return this.afAuth.authState.pipe(
    take(1),
    switchMap(user => {
      return this.dbAction.getUserProfileData(user.email)
    }),
    map(profile => !!(profile.length && profile[0].admin),
    tap(isAdmin => {
      if (isAdmin) {
        console.log('admin user, you shall pass')
      } else {
        console.log('non-admin user, go home')
        this.router.navigate(['/']);
      }
    })
  )

}

I have done many guard implementations with Angular/Firebase and have some opinions about what works best – I think this role based auth screencast might help you improve on your current approach

Answered By – JeffD23

Answer Checked By – Cary Denson (AngularFixing Admin)

Leave a Reply

Your email address will not be published.