4

The AngularFire auth guard doc shows different ways to allow authentication.

Only admin users:

const editorOnly = pipe(customClaims, map(claims => claims.role === "editor"));

Self only:

const onlyAllowSelf = (next) => map(user => !!user && next.params.userId === user.uid);

My question, is how to combine the two that either an editor/admin or the user himself can open the component.

f.khantsis
  • 3,256
  • 5
  • 50
  • 67

3 Answers3

5

You can create a combineAuthPipes function, which uses forkJoin under the covers:

const combineAuthPipes = (authPipes: AuthPipe[]) => 
    switchMap((t: Observable<firebase.User>) => forkJoin(authPipes.map(x => x(t))))

const loggedInThenRedirect = pipe(
    map((t: firebase.User) => of(t)),
    combineAuthPipes([
        loggedIn,
        customClaims as AuthPipe,
        hasCustomClaim('admin'),
        hasCustomClaim('editor')
    ]),
    map(([isLoggedIn, customClaimList, admin, moderator]) => {
        return {
            loggedIn: isLoggedIn,
            customClaims: customClaimList,
            admin, 
            moderator
        }
    }),
    map((t) => {
        if (t.admin) {
            return true
        }
        if (!t.loggedIn) {
            return ['login']
        }
        return ['unauthorized']
    })
)

Use like this:

export const routes: Routes = [
    { 
        path: '', component: SomeComponent,
        pathMatch: 'full',
        canActivate: [
            AngularFireAuthGuard
        ],
        data: { authGuardPipe: () => loggedInThenRedirect }
    }
]
Michael Kang
  • 52,003
  • 16
  • 103
  • 135
4

As long as your final function adheres to the AuthPipe interface (mapping Observable<firebase.User | null> to Observable<boolean | string | string[]>) you can compose any custom pipe.

The solution I went for used:

  1. pipe operator to pipe into the final AuthPipe interface.
  2. forkJoin to join together the results from multiple AngularFireAuth pipes.
  3. map to convert the intermediary results to boolean | string | string[].

With this approach you can use the results of multiple AuthPipes when implementing canActivate logic.

(essentially a cleaner version of @pixelbits answer)

import { AngularFireAuthGuard, AuthPipe, emailVerified, loggedIn } from '@angular/fire/compat/auth-guard';
import { forkJoin, of, pipe } from 'rxjs';

const combined: AuthPipe = pipe(
  mergeMap((user) => forkJoin([loggedIn(of(user)), emailVerified(of(user))])),
  map(([isLoggedIn, isEmailVerified]) => {
    console.log({ isLoggedIn, isEmailVerified });
    if (!isLoggedIn) {
      return '/auth/login';
    }
    if (!isEmailVerified) {
      return '/auth/verify-email';
    }
    return true;
  }),
);

// usage
{
   component: MyComponent,
   canActivate: [AngularFireAuthGuard],
   data: { authGuardPipe: () => combined },
},

A note on the customClaims AuthPipe: it expects firebase.User (does not allow for null), so you can either:

  1. Use a ternary operator: user ? customClaims(of(user)) : of(null) or
  2. Call anyways customClaims(of(user as any)), which returns an empty array when user is null (source)
jrasm91
  • 394
  • 2
  • 10
2

This is how I got it to work, but perhaps there is a more eloquent answer:

const selfOrAdmin: AuthPipeGenerator = next => (users$) =>
  combineLatest([customClaims(users$).pipe(map(claims => claims['role'] === 'editor')),
    users$.pipe(map(user => !!user && next.params.userId === user.uid))])
  .pipe(map(([isAdmin, isSelf]) => isAdmin || isSelf))
;
f.khantsis
  • 3,256
  • 5
  • 50
  • 67