angular / angularfire

Angular + Firebase = ❤️
https://firebaseopensource.com/projects/angular/angularfire2
MIT License
7.69k stars 2.19k forks source link

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

Open AdditionAddict opened 2 years ago

AdditionAddict commented 2 years ago

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

google-oss-bot commented 2 years ago

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

davideast commented 2 years ago

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

DaveA-W commented 1 year ago

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 {}
rubenheymans commented 1 year ago

did you find a solution for this?

AdditionAddict commented 1 year ago

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

AbdulSamadMalik commented 1 year ago
// 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'])
  );