0
votes

I am able to login using firebase, however in my app I have 3 levels of users. Let's call them admin, user, editor. I keep user's role in custom claims provided by Firebase.

In AuthGuard, I pass data expectedRoles = ['admin', 'editor'], adding roles I want to allow for a specific route. And I want to do redirecting to one's own page, such as editor trying admin route will be returned to editor dashboard.

This is my AuthGuard's canActivate function:

return this.auth.user.pipe(
  map(user => {
    if (user) {
      user.getIdTokenResult().then((idTokenResult) => {
        if (idTokenResult.claims.admin) {
          if (expectedRoles.indexOf('admin') > -1) {
            return true;
          } else {
            // return back to admin dashboard
            this.router.navigate(['/admin']);
          }
        } else if (idTokenResult.claims.editor) {
          if (expectedRoles.indexOf('editor') > -1) {
            return true;
          } else {
            // return back to editor dashboard
            this.router.navigate(['/editor']);
          }
        } else if (idTokenResult.claims.user) {
          if (expectedRoles.indexOf('user') > -1) {
            return true;
          } else {
            // return back to user dashboard
            this.router.navigate(['/user']);
          }
        } else {
          // Unexpected claim, better logout, TODO; display something
          this.router.navigate(['/auth/logout']);
        }
      });
    } else {
      // User is not authenticated
      // Check if we are expecting a guest
      if (expectedRoles.indexOf('guest') > -1) {
        return true;
      } else {
        this.router.navigate(['/auth/login']);
      }
    }
  })
).first();

Before adding user.getIdTokenResult().then(...) it was working fine, and I am aware it is probably due to not waiting the async call to parse the custom claims. How can I work around this?

2

2 Answers

2
votes

Use switchMap instead of map, make the function async and await the result. switchMap accepts the promise that is returned by the async function. Then inside you can user await.

return this.auth.user.pipe(
  // switchMap instead of map
  // async function call
  switchMap(async (user) => {
    if (user) {
      // await here
      await user.getIdTokenResult().then((idTokenResult) => {
        if (idTokenResult.claims.admin) {
          if (expectedRoles.indexOf('admin') > -1) {
            return true;
          } else {
            // return back to admin dashboard
            this.router.navigate(['/admin']);
          }
        } else if (idTokenResult.claims.editor) {
          if (expectedRoles.indexOf('editor') > -1) {
            return true;
          } else {
            // return back to editor dashboard
            this.router.navigate(['/editor']);
          }
        } else if (idTokenResult.claims.user) {
          if (expectedRoles.indexOf('user') > -1) {
            return true;
          } else {
            // return back to user dashboard
            this.router.navigate(['/user']);
          }
        } else {
          // Unexpected claim, better logout, TODO; display something
          this.router.navigate(['/auth/logout']);
        }
      });
    } else {
      // User is not authenticated
      // Check if we are expecting a guest
      if (expectedRoles.indexOf('guest') > -1) {
        return true;
      } else {
        this.router.navigate(['/auth/login']);
      }
    }
  })
).first();

Just a hint unrelated to this solution - better than load token claim in the AuthGuard function, you should load it after the user logs in and store it in the store the same way you store the information from the login response. This will be much faster.

0
votes

Try using the rxjs pipes for inner observable calls: flatMap and flatMapTo or mergeMap or mergeMapTo. In your specific case I think mergeMap is the way to go.

Feel free to read about those pipes in rxjs, they might be the solution