Search code examples
angularrxjsfirebase-authenticationangularfire2angularfire

AngularFire Auth exchange FireBase token for my platform's token before re-direct


I'm using AngularFire to facilitate auth to a Firebase Auth user pool, and the authentication is working fine.

However, after Firebase auth and BEFORE redirecting from the login page to one of my guarded webapp pages, I need to exchange the Firebase token for a JWT token from my platform.

I think the way to do this is to implement the logic to call out to my platform token API in a router guard.

However when I do this, I get this error:

TypeError: source.lift is not a function

Here is my app-routing.module.ts, if I replace switchMap for map and remove async/await (dont make it return a promise or do async logic within the callback) things work fine - however I'm then not calling my API.

import { NgModule, Injector } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { canActivate, redirectUnauthorizedTo, redirectLoggedInTo } from '@angular/fire/auth-guard';
import { switchMap } from 'rxjs/operators';
import * as firebase from 'firebase';

import { LoginComponent } from './login/login.component';
import { InvestconfigComponent } from './investconfig/investconfig.component';
import { setWebIdentityCredentials } from './shared/auth.service';

//THIS IS THE IMPORTANT method
const redirectLoggedInAferSetAwsCreds = switchMap(async (user: firebase.User) => {
  // Call off to my backend to exchange FBase token for platform token..
  await setWebIdentityCredentials(user);
  return user ? ['config'] : true;
});
const redirectUnauthorizedToLogin = redirectUnauthorizedTo(['login']);

const routes: Routes = [
  { path: '', redirectTo: '/config', pathMatch: 'full' },
  { path: 'login', component: LoginComponent, ...canActivate(redirectLoggedInAferSetAwsCreds) },
  { path: 'config', component: InvestconfigComponent, ...canActivate(redirectUnauthorizedToLogin) },
  { path: '**', redirectTo: '/config' },
];

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

Why doesn't this work? Is there a better way to solve my problem?


Solution

  • I got it working, but I totally mis-understood when the AngularFire auth guards get called. IMO you should not call out to get creds inside your guard.

    Here is the guard that works if you need to await on a promise:

    const guardAndSetAwsCreds = pipe(
      tap(async (user: firebase.User) => {
        await setWebIdentityCredentials(user);
      }),
      map((user: firebase.User) => {
        return user ? true : ['login'];
      }),
    );
    

    tap() causes no side effect, and passes the orig obj (user in this case) onto map().

    I was under the incorrect impression that when an AuthFire auth method completed successfully the AuthFire guard was called via a subscription. That is not the case. Here is an example of an AuthFire auth method:

    this.afAuth.auth.signInWithEmailLink(email, window.location.href)
    

    Because I no longer had a timing issue, I simply callout to get a platform token in my login method:

      async signinWithEmailLink() {
        // Confirm the link is a sign-in with email link.
        if (this.afAuth.auth.isSignInWithEmailLink(window.location.href)) {
          // Additional state parameters can also be passed via URL.
          // This can be used to continue the user's intended action before triggering
          // the sign-in operation.
          // Get the email if available. This should be available if the user completes
          // the flow on the same device where they started it.
          let email = window.localStorage.getItem('emailForSignIn');
          if (!email) {
            // User opened the link on a different device. To prevent session fixation
            // attacks, ask the user to provide the associated email again. For example:
            email = window.prompt('Please provide your email for confirmation');
          }
    
          // The client SDK will parse the code from the link for you.
          const res = await this.afAuth.auth.signInWithEmailLink(email, window.location.href)
    
          window.localStorage.removeItem('emailForSignIn');
    
          if (res.additionalUserInfo.isNewUser) {
            //User does NOT already exist in system (added by admin) might be sign-up from random dude from internet
            throw new Error('An admin must add you first');
          }
    
          await setWebIdentityCredentials(res.user);
        }
      }
    

    My route guards are super simple now:

    const routes: Routes = [
      { path: '', redirectTo: '/config', pathMatch: 'full' },
      { path: 'login', component: LoginComponent, ...canActivate(redirectLoggedInTo(['config'])) },
      { path: 'config', component: InvestconfigComponent, ...canActivate(redirectUnauthorizedTo(['login'])) },
      { path: '**', redirectTo: '/config' },
    ];