Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Firestore emulator calls Google APIs to check auth token #2656

Closed
jornetsimon opened this issue Nov 15, 2020 · 17 comments
Closed

Firestore emulator calls Google APIs to check auth token #2656

jornetsimon opened this issue Nov 15, 2020 · 17 comments

Comments

@jornetsimon
Copy link

jornetsimon commented Nov 15, 2020

This issue is kinda related to #2637 since it deals with the same end result.
I figured this bug deserves its own issue though, because it seems so be related to Firestore this time.

Version info

Angular:
10.2.0

Firebase:
8.0.1

AngularFire:
6.1.0-rc.3

How to reproduce these conditions

Failing test unit, Stackblitz demonstrating the problem

https://github.com/jornetsimon/af-auth-debug
Updated with commit reproducing the issue.

Steps to set up and reproduce

  • Set up the Authentication & Firestore emulators using the new 6.1 injection tokens.
  • Set up an authentication method (email/password for example) and implement it on a page
  • Subscribe to authState
  • Inject AngularFireStore on a separate routed component (lets call it ComponentB)
  • Sign up : no issue
  • Reload the app, navigate to ComponentB :
  • Now load the app directly on the route pointing to ComponentB (e.g. /componentb): a call to Firebase servers (googleapis.com) is made, returning a 400 INVALID_ID_TOKEN error. The user is therefore logged out.

Sample data and security rules

Providers config:`

providers: [
  {
    provide: USE_FIRESTORE_EMULATOR,
    useValue: environment.useEmulators ? ['localhost', 8888] : undefined,
  },
  {
    provide: USE_AUTH_EMULATOR,
    useValue: environment.useEmulators ? ['localhost', 9099] : undefined,
  },
],

Debug output

When using emulators :
98449248-42ef6a00-2132-11eb-94b1-e101862f56a8

Expected behavior

The AngularFirestore service should target the emulator to verify the auth token.

Actual behavior

There is no problem when authenticating THEN navigating to a component using the AngularFirestore service.

But when loading the app directly through the firestore component endpoint, it seems like it doesn't have the time to be aware of the emulator config...

Note : The issue is the same with 6.1.0-rc.2 and 6.1.0-rc.3.

@jamesdaniels
Copy link
Member

This is interesting, it appears to be trigged by the fact that AngularFireAuth dynamically imports firebase/auth... if you add import 'firebase/auth'; directly in the injects-firestore component it functions correctly.

I wonder if when useEmulator is called in Firestore, before auth is loaded (because it's dynamic), and then Firestore assumes production auth... which is weird because you called useEmulator.

I don't actually see an easy way around this with the current API... If AngularFirestore was a lazy loading module I would pull in AngularFireAuth as an optional and ensure it was initialized first. Maybe AngularFirestore can inject the USE_EMULATOR token from auth and give a warning at the very least.

jamesdaniels added a commit that referenced this issue Nov 17, 2020
#2661)

* Adding global instance caches to the modules, so they don't freak out when HMR is enabled (#2655). This takes on `globalThis` as a needed polyfill for many environments.
* If injected settings for modules are changed after they are initialized you will receive a warning and the prior instance will be returned (ignoring the changes), this is especially important for HMR. If HMR is detected there will be an additional warning suggesting they do a full reload to see the changes.
* Added a polyfill table and notes about why we version like we do
* Adding more convoluted stuff to my sample app to flex AngularFire
* Internal cleanup on AngularFireAnalytics
* AngularFireAnalytics will now wait for UserTrackingService to detect the user before sending the screen_view event, if UserTrackingService has been provided
* Adding a warning if the Auth Emulator is detected in conjunction with AngularFirestore and AngularFireDatabase as they will invalidate the emulated auth token before the dynamic import of `firebase/auth` is completed (#2656)
* Warn if we absorbed an error keeping Firestore persistence from enabling
* Logging sign_up and login events in UserTrackingService
* Adding credential observer to AngularFireAuth
@jamesdaniels
Copy link
Member

I've done the best I could with the current API, there's now a warning in the soon to be cut 6.1.0.rc-4

@Gbuomprisco
Copy link

Gbuomprisco commented Nov 21, 2020

I am trying to work this around by importing "firebase/auth" from the very root of the application, but the issue remains. Any idea?

if it can help anyone, for the time being, my workaround is this:

// add provider in you AppModule
{
    provide: APP_INITIALIZER,
    multi: true,
    useFactory: useEmulatorProvider,
  }

function useEmulatorProvider() {
  return () => {
    if (isEmulator) {
      const firebase = require('firebase/app').default;
      firebase.auth().useEmulator(`http://localhost:9099`);
    }
  };
}

@jamesdaniels
Copy link
Member

The trouble is that no matter how you slice it, unless you initialize auth and firestore/database at the same time inline in your APP_INITIALIZER, the behavior is indeterminate. Does auth load first or database/firestore?... no way to control with the exiting API.

I will be prioritizing the addition of a lazy version of Database/Firestore/Storage in the next AngularFire minor, I've been putting that off for way too long. Not only would a lazy versions reduce your main bundle size but I will be able to ensure that AngularFireAuth is initialized first, if it's been provided.

@jamesdaniels
Copy link
Member

jamesdaniels commented Nov 21, 2020

Also I could see filing this as a bug with the Firebase JS SDK... if you initialize Database/Firestore with useEmulator (before you initialize Auth) it seems like it doesn't make sense to error and destroy emulated Auth credentials. At the very least it could catch the 400 and just warn if useEmulator had been called or test if they were emulated credentials and proceed using them for the emulator.

FWIW Storage doesn't seem to have the same problem.

@Feiyang1 any thoughts?

@jamesdaniels
Copy link
Member

FYI @jhuleatt we're going to run into a similar issue with ReactFire and useEmulator since it's completely lazy loaded.

@jamesdaniels
Copy link
Member

In thinking about it more, I consider this unexpected behavior on the JS SDK side and have filed an issue.

@jornetsimon
Copy link
Author

In thinking about it more, I consider this unexpected behavior on the JS SDK side and have filed an issue.

Yeah, when I tried reproducing to isolate the cause, I didn't encounter the bug when using the vanilla SDK, so I figured this was about AngularFire and create the issue here.

@jamesdaniels
Copy link
Member

@jornetsimon yeah, it's a race condition. Since we're dynamically importing auth AngularFire is much more likely to see it.

@tvhoang91
Copy link

Hi all,

I was having the same issue.
And this is the work around for me.
Just adding the route guard which always return true.
Then the Firebase Auth will be ready first.

import { canActivate, customClaims } from '@angular/fire/auth-guard'

const avalableToAll = () =>
  pipe(
    customClaims,
    map(() => true)
  )

const routes: Routes = [
  {
    path: 'companyview/:id',
    component: CompanyViewComponent,
    ...canActivate(avalableToAll),
  },
]

@prodtos
Copy link

prodtos commented Feb 22, 2021

I think i found a better workaround, but you guys can be the judge of that.

Just use this in the component.

  constructor(private firestore: AngularFirestore) {
    if (environment.useEmulators) {
      firestore.firestore.app.auth().useEmulator('http://localhost:9099');
    }
  }

@WUKS87
Copy link

WUKS87 commented Mar 11, 2021

I think i found a better workaround, but you guys can be the judge of that.

Just use this in the component.

  constructor(private firestore: AngularFirestore) {
    if (environment.useEmulators) {
      firestore.firestore.app.auth().useEmulator('http://localhost:9099');
    }
  }

@prodtos Where you added that code? I tried to add in main app.component.ts but I get this error:
TypeError: this.firestore.firestore.app.auth is not a function

@WUKS87
Copy link

WUKS87 commented Mar 11, 2021

Also, I tried this:
constructor(private _angularFireAuth: AngularFireAuth) { this._angularFireAuth.useEmulator('http://localhost:9099'); }

but still I get the same error.

@newable
Copy link

newable commented Mar 17, 2021

@WUKS87 you can try with something like:

export function initializeApp(afAuth: AngularFireAuth): () => Promise<null> {
  return () => {
    return new Promise((resolve) => {
      if (!environment.emulators) {
        return resolve();
      } else {
        afAuth.useEmulator(`http://${location.hostname}:9099/`).then(() => {
          resolve();
        });
      }
    });
  };
}

@NgModule({
  ...
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [AngularFireAuth],
      useFactory: initializeApp
    },
    { provide: USE_AUTH_EMULATOR, useValue: environment.emulators ? ['localhost', 9099] : undefined },
    { provide: USE_FIRESTORE_EMULATOR, useValue: environment.emulators ? ['localhost', 8080] : undefined },
    { provide: USE_FUNCTIONS_EMULATOR, useValue: environment.emulators ? ['localhost', 5001] : undefined },
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

@WUKS87
Copy link

WUKS87 commented Apr 2, 2021

@newable Yup, works like a charm! Thanks man :)

@romanhuba
Copy link

The solution provided by @newable works as long as you can make sure that afAuth.useEmulator(..) is called before any of the AngularFire modules (Firestore, Database, etc.) is initialized. This solution breaks when you want to use for example Firestore in an APP_INITIALIZER factory, or when using NgRx Effects, in which you likely have AngularFirestore injected in the constructor. The problem is that you cannot control in what order the APP_INITIALIZER's dependencies are instantiated and also it doesn't really matter, because at the point in time when your APP_INITIALIZER with afAuth.useEmulator(..) is running, the other AngularFire module (be it Firestore that you want to inject in a different APP_INITIALIZER ) has already been created before you manage to call afAuth.useEmulator(..). NgRx Effects on other hand are being instantiated even before the APP_INITIALIZERs are done, so in case of NgRx Effects, you are out of luck even with an APP_INITIALIZER. I have seen a workaround for NgRx Effects where you provide the effects in an injection token after the APP_BOOTSTRAP_LISTENER but again this leads to issues where the effects are registered too late and also providing working feature effects seemed to be an issue.

What I resorted to was the following and it takes care of both issues.

  1. Obtain the Firebase configuration before the application is bootstrapped in the main.ts
  2. Initialize the Firebase app with resolved configuration using the Firebase SDK
  3. If you want to use Firebase Auth emulator, call firebase.auth().useEmulator(..) after you initialize the Firebase app
  4. Provide the Firebase configuration using the AngularFire FIREBASE_OPTIONS token in the platformBrowserDynamic extraProviders argument
  5. Bootstrap your application
  6. In your app or core module (the module where you import AngularFire) import only AngularFireModule without the initializeApp(..) call
  7. Import other AngularFire modules as normal
  8. For using emulators with other AngularFire modules use the provided injection tokens as normal
  9. Do not provide USE_EMULATOR injection token for AngularFireAuthModule

The Firebase SDK initialization is not needed when Auth emulator is not being used, so in production builds the firebase-init-app.ts can be replaced with firebase-init-app.prod.ts that exports an empty initialize function. In that way, you can rely on the dynamic import provided by AngularFire when Auth emulator is not needed.

main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { FIREBASE_OPTIONS } from '@angular/fire';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { load as loadFirebaseConfig } from './firebase-fetch-config';
import { initialize as initializeFirebaseApp } from './firebase-init-app';

if (environment.production) {
  enableProdMode();
}

loadFirebaseConfig()
  .then((config) => {
    initializeFirebaseApp(config);

    return platformBrowserDynamic([
      {
        provide: FIREBASE_OPTIONS,
        useValue: config,
      },
    ]).bootstrapModule(AppModule);
  })
  .catch((err) => console.error(err));

firebase-init-app.ts

import firebase from 'firebase';
import { environment } from './environments/environment';

export const initialize = (config: any) => {
  firebase.initializeApp(config);

  if (environment.firebase.emulators?.auth) {
    firebase.auth().useEmulator(environment.firebase.emulators.auth);
  }
};

core.module.ts

import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';

import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFirestoreModule, USE_EMULATOR as FIRESTORE_EMULATOR_CONFIGURATION } from '@angular/fire/firestore';

import { AppStoreModule } from '@app-store/app-store.module';
import { TranslocoRootModule } from '../transloco/transloco-root.module';
import { environment } from '../../environments/environment';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,

    TranslocoRootModule,

    AppStoreModule,

    AngularFireModule,
    AngularFirestoreModule,
    AngularFireAuthModule,
  ],
  providers: [
    {
      provide: FIRESTORE_EMULATOR_CONFIGURATION,
      useValue: environment.firebase.emulators?.firestore,
    },
  ],
})
export class CoreModule {}

@thomi137
Copy link

thomi137 commented May 22, 2021

@amilor's solution works perfectly and I am using ngrx... Thanks a lot!

jornetsimon added a commit to jornetsimon/moumou-pad that referenced this issue Oct 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants