9
votes

My Angular project successfully uses the AngularFireAuth injectable from @angular/fire to connect to a local Firebase authentication emulator. I used these sources as references for my implementation:

However, as soon as I reload the page the login state is lost and I have to log in again.

First I tried setting the persistence like this, in an Angular component constructor:

constructor( private _fireauth: AngularFireAuth) {
    this._fireAuth.useEmulator(`http://${location.hostname}:9099/`);
    this._fireauth.setPersistence(firebase.auth.Auth.Persistence.LOCAL);
}

which made no difference. Then I noticed in the browser network console that apparently the firebase library does find the locally stored session after pageload but initially attempts to contact google servers to verify it, which of course fails, because it's a local emulator session:

enter image description here

This seemed to make sense because _fireAuth.useEmulator() is only called in the component constructor. Other parts of the app such as the AngularFireAuthGuard probably try accessing the authentication status way earlier than that. A few attempts to call useEmulator() before any other part of the app might attempt to access the authentication status failed:

  • Attempt1: Calling useEmulator() in an @Injectable({providedIn: 'root'}) aka service constructor
  • Attempt2: Providing an APP_INITIALIZER which executes code before the application is even bootstrapped:
export function initializeApp1(afa: AngularFireAuth): any {
  return () => {
    return new Promise(resolve => {
      afa.useEmulator(`http://${location.hostname}:9099/`);
      setTimeout(() => resolve(), 5000);
    });
  };
}

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    ...
  ],
  providers: [
     AngularFireAuth,
     {
       provide: APP_INITIALIZER,
       useFactory: initializeApp1,
       deps: [AngularFireAuth],
       multi: true
     }
  ],
  bootstrap: [AppComponent]
})

  providers: [
     {
       provide: USE_EMULATOR,
       useValue: [location.hostname, 9099]
     },
  ]

I thought especially Attempt3 would set the emulator mode as early as possible because how much earlier could it be set than in the constructor? The Injectable seem to work, as the browser console prints the usual warning message on pageload: auth.esm.js:133 WARNING: You are using the Auth Emulator, which is intended for local testing only. Do not use with production credentials.. However, the request to googleapis.com is still fired every time a stored session in the local indexedDB is detected on pageload and I'm logged out afterwards. Also worth noting: In Attempt2 I use a 5 second timeout to slow down the app initialization noticeably and the request to googleapis.com is fired instantly after pageload, way before Angular has finished initializing.

At this point I don't know what to try anymore. @angular/fire is obviously build upon the basic Firebase JavaScript library, so I might have to set emulator mode even earlier, but maybe I'm walking in the completely wrong direction here?

================================================================

[EDIT:] It seems that combining Attempt 2 with Attempt 3 works, as long as the initializeApp1() method uses a setTimout() of at least approx. 100ms. When I remove the timeout or set it to something as low as 10ms the login ist lost. Any timeout longer than 100ms works too. At this point it looks very much like a race condition?

Working example:

export function initializeApp1(afa: AngularFireAuth): any {
  return () => {
    return new Promise(resolve => {
      afa.useEmulator(`http://${location.hostname}:9099/`);
      setTimeout(() => resolve(), 100); // delay Angular initialization by 100ms
    });
  };
}

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    ...
  ],
  providers: [
     // Provide the AngularFireAuth constructor with the emulator parameters
     {
       provide: USE_EMULATOR, 
       useValue: [location.hostname, 9099]
     },
     // Delay the app initialization process by 100ms
     {
       provide: APP_INITIALIZER,
       useFactory: initializeApp1,
       // for some reason this dependency is necessary for this solution to work.
       // Maybe in order to trigger the constructor *before* waiting 100ms?
       deps: [AngularFireAuth],  
       multi: true
     }
  ],
  bootstrap: [AppComponent]
})

I also noticed this comment in the AngularFireAuth source code. So far I don't really understand what's happening, but maybe the issue is related:

// HACK, as we're exporting auth.Auth, rather than auth, developers importing firebase.auth (e.g, import { auth } from 'firebase/app') are getting an undefined auth object unexpectedly as we're completely lazy. Let's eagerly load the Auth SDK here. There could potentially be race conditions still... but this greatly decreases the odds while we reevaluate the API.

2
Struggling with the same issue. The auth emulator is fairly new, and it may be a bug/missing feature that needs to be corrected in one of the libs. - jornare
I just added a few new insights. It looks like a race condition in the constructor of the AngularFireAuth injectable. - codepearlex
I also found that comment, but after debugging a little more I'm more uncertain. - jornare
I managed to get around it by cleaning up my dependencies so that firestore was initialized after auth. I found I had a dependency to firestore in my custom auth service. - jornare

2 Answers

0
votes

I've tweaked the initialize app function in my code, to use rxjs instead of promises.

export const initializeApp = (angularFireAuth: AngularFireAuth): (() => Observable<boolean>) => {
    if (!environment.firebaseConfig.useEmulator) {
        return () => of(true);
    } else {
        return () => of(true).pipe(
            delay(100),
            tap(_ => {
                angularFireAuth.useEmulator('http://localhost:9099/');
            })
        );
    }
};
0
votes

The following solution worked for me -

Add this to the top of app.module.ts (before @NgModule) -

const firebaseConfig = {
  ...
};

import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import 'firebase/functions';

const app = firebase.initializeApp(firebaseConfig, 'myApp');
if (environment.useEmulators) {
  app.auth().useEmulator('http://localhost:9099');
  app.firestore().useEmulator('localhost', 8080);
  app.functions().useEmulator('localhost', 5001);
}

And then further down in your imports section:

  imports: [
    ...
    AngularFireModule.initializeApp(firebaseConfig, 'myApp'),
    ...
  ],

I'm guessing this method "grabs" the Auth service earlier than AngularFire. But as you mentioned, the authentication emulator is quite new, hopefully in one of the next releases it will be fixed. Based on this github post.