angularfire icon indicating copy to clipboard operation
angularfire copied to clipboard

Auth Guard error when providing authpipe direct rather than as pipe generator (v9 new API not compat)

Open AdditionAddict opened this issue 3 years ago • 6 comments
trafficstars

Version info

Angular: 14

Firebase: 9

AngularFire: ^7.4.1

Other (e.g. Ionic/Cordova, Node, browser, operating system): Node, Firefox/Chrome, Windows

How to reproduce these conditions

I've created stackoverflow Q&A of the issue https://stackoverflow.com/questions/73799781/how-to-create-auth-guard-in-angular-fire-v9-with-new-api-not-compat I believe this is a bug and not a documentation issue given the naming of authGuardPipe

When using guard below and navigating to a guarded feature as a user with email not verified (used emulators) this throws the error TypeError: Unable to lift unknown Observable type triggered by https://github.com/angular/angularfire/blame/master/src/auth-guard/auth-guard.ts#L21

const redirectUnauthorizedAndUnverifiedToAuth: AuthPipe = map((user: User | null) => {
  // if not logged in, redirect to `auth`
  // if logged in and email verified, allow redirect
  // if logged in and email not verified, redirect to `auth/verify`
  return !!user ? (user.emailVerified ? true : ['auth', 'verify']) : ['auth']
})

Replacing redirectUnauthorizedAndUnverifiedToAuth in data : { authGuardPipe: redirectUnauthorizedAndUnverifiedToAuth } with authPipeGenerator fixes

Failing test unit, Stackblitz demonstrating the problem

Can't produce stackblitz due to error https://github.com/stackblitz/core/issues/2039

Steps to set up and reproduce

Created basic auth & my-feature modules, v9 new API angular imports

...
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { provideAuth, getAuth, connectAuthEmulator } from '@angular/fire/auth';
import {
  provideFirestore,
  getFirestore,
  connectFirestoreEmulator,
} from '@angular/fire/firestore';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    provideFirebaseApp(() => initializeApp(firebase)), <--- provide firebase
    provideAuth(() => {
      const auth = getAuth();
      // connectAuthEmulator(auth, 'http://localhost:9099')
      return auth;
    }),
    provideFirestore(() => {
      const firestore = getFirestore();
      // connectFirestoreEmulator(firestore, 'localhost', 8080)
      return firestore;
    }),
  ],
  declarations: [AppComponent, HelloComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

app-routing.module.ts

import { NgModule } from '@angular/core';
import { User } from '@angular/fire/auth';
import { AuthGuard, AuthPipe, AuthPipeGenerator } from '@angular/fire/auth-guard';
import { Routes, RouterModule } from '@angular/router';
import { map } from 'rxjs';

const redirectUnauthorizedAndUnverifiedToAuth: AuthPipe = map((user: User | null) => {
  // if not logged in, redirect to `auth`
  // if logged in and email verified, allow redirect
  // if logged in and email not verified, redirect to `auth/verify`
  return !!user ? (user.emailVerified ? true : ['auth', 'verify']) : ['auth']
})
const authPipeGenerator: AuthPipeGenerator = () => redirectUnauthorizedAndUnverifiedToAuth

const routes: Routes = [
  {
    path: "auth",
    loadChildren: () => import("./auth/auth.module").then(m => m.AuthModule)
  },
  {
    path: "my-feature",
    loadChildren: () => import("./my-feature/my-feature.module").then(m => m.MyFeatureModule),
    canActivate: [AuthGuard],
    data: { authGuardPipe: redirectUnauthorizedAndUnverifiedToAuth }
  },
  { path: "", redirectTo: "auth", pathMatch: "full" },
  { path: "**", redirectTo: "auth" }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Expected behavior

if you provide pipe this should work esp. given the name authGuardPipe

  {
    path: "my-feature",
    loadChildren: () => import("./my-feature/my-feature.module").then(m => m.MyFeatureModule),
    canActivate: [AuthGuard],
    data: { authGuardPipe: redirectUnauthorizedAndUnverifiedToAuth }
  },

Actual behavior

Error TypeError: Unable to lift unknown Observable type

AdditionAddict avatar Sep 21 '22 12:09 AdditionAddict

This issue does not seem to follow the issue template. Make sure you provide all the required information.

google-oss-bot avatar Sep 21 '22 12:09 google-oss-bot

Hey! Can you provide a repo or stackblitz that showcases this?

davideast avatar Oct 04 '22 04:10 davideast

Confirming this is still an issue with v9.4. Since the error is swallowed, this had me spinning wheels for half a day before discovering this thread. Thank you @AdditionAddict

In my case, I have my pipe defined as a static class function rather than as a const:

export class AppAuthGuard extends AngularFireAuthGuard {

  static hasPermissionPipe(role?: string): AuthPipe {
    return pipe(
      switchMap((user: firebase.User | null) => (user ? user.getIdTokenResult() : of(null))),
      map(result =>
        result
          ? // If signed in, check the user's role claim
            !role || result.claims['role'] === role || ['forbidden']
          : // Otherwise navigate to the sign-in page
            ['sign-in'],
      ),
    );
  }
  
  static hasPermissionGenerator(role?: string): AuthPipeGenerator {
    return () => AppAuthGuard.hasPermissionPipe(role);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
    return this.canActivate(route, state);
  }
}

With the routes below, navigating to /administration and child paths works fine, whereas /crud does not. Stepping past a breakpoint in auth-guard.ts, I see the same TypeError: Unable to lift unknown Observable type.


const routes: Routes = [
  {
    path: 'administration',
    data: { authGuardPipe: AppAuthGuard.hasPermissionGenerator('admin') },
    canActivate: [AppAuthGuard],
    canActivateChild: [AppAuthGuard],
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
  },
  {
    path: 'crud',
    data: { authGuardPipe: AppAuthGuard.hasPermissionPipe('admin') },
    canActivate: [AppAuthGuard],
    canActivateChild: [AppAuthGuard],
    loadChildren: () => import('./crud/crud.module').then(m => m.CrudModule),
  },  
  {
    path: '',
    loadChildren: () => import('./main/main.module').then(m => m.MainModule),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

DaveA-W avatar Dec 15 '22 12:12 DaveA-W

did you find a solution for this?

rubenheymans avatar May 08 '23 19:05 rubenheymans

@rubenheymans opening post has "correct" answer, issue details unexpected usage

AdditionAddict avatar May 09 '23 07:05 AdditionAddict

// changing this
const adminGuard = pipe(
  customClaims,
  map((claims) => claims.role === 'admin' || ['login'])
);

// to this, fixes the issue for me
const adminGuard = () =>
  pipe(
    customClaims,
    map((claims) => claims.role === 'admin' || ['login'])
  );

AbdulSamadMalik avatar Jul 23 '23 01:07 AbdulSamadMalik