isAuthenticated fails on login, works on refresh

I’m writing an Angular npm library to streamline Auth0 implementation for my company and running into a strange bug. I’ve left the “auth0Client$” and “isAuthenticated$” streams virtually unchanged from the example documentation, yet immediately after login the “isAuthenticated$” stream returns false when subscribed to. If i refresh the page, it works as expected. As far as I can tell, my flow is no different than the example: Login -> Redirect to Auth0 -> return to /callback route -> configure/setup Auth0 client on app entry -> run localAuthSetup.

localAuthSetup immediately calls the isAuthenticated& stream and if I put a tap function in the stream and console log the value its false when re-entering the app from login. However, if I add this to my app.component:

setTimeout(()=>{
     this.authService.isAuthenticated$(console.log);
}, 2000);

it will return true, even on initial login. Which suggests to me that client.isAuthenticated() is immediately returning false on app entry, even when its returning back from a successful login.

Any ideas?

Edit

Well, I think i know why its doing this, but i don’t know why the example would tell you to set it up like this. isAuthenticated only works after handleRedirectCallback has been called, yet the isAuthenticated$ stream is public and open to be subscribed to immediately on app load, before handleRedirectCallback is complete. So everything subscribes to that stream on app load, gets a “false” result because handleRedirectCallback isn’t complete, and then once its complete all those observables have already completed and don’t re-poll for the new value.

I always assumed that they left isAuthenticated$ public because a lot of people like working with observables, but now I just think it was a mistake.

Edit 2

I’m pretty certain this fixed my issue, although I’m still bitter that the docs always have mistakes like this that I have to correct for:

    // Create internal subject and stream
    this._internalIsAuthenticatedSubject$ = new BehaviorSubject<boolean>(null);
    const internalIsAuthenticatedStream = this._internalIsAuthenticatedSubject$.asObservable();

    // Create standard authentication stream as per docs
    const externalIsAuthenticatedStream = this.auth0Client$.pipe(
      concatMap((client: Auth0Client) => from(client.isAuthenticated())),
      tap(res => this.loggedIn = res));

    // On login, client.isAuthenticated() will be false until handleAuthCallback
    // is finished.  This means that subscriptions that are used on app startup will return false on app load and won't get the
    // authenticated value until refresh. To fix this, we patch in the internal stream which is updated after the handleAuthCallback
    // finishes. That way the most fresh value is sent to the observers. On refresh, the internal stream will provide only the initial
    // 'null' value which is filtered.
    this.isAuthenticated$ = merge(internalIsAuthenticatedStream, externalIsAuthenticatedStream)
      .pipe(filter(val => val !== null));

Then after “handleAuthCallback” completes I run:

this._internalIsAuthenticatedSubject$.next(loggedIn);

to push the most fresh logged in state out to all the observers.

Brandon, thank you for the detailed feedback you have shared and for bringing this up. I apologize in advance for any blocker or speed bump the guidance may have caused.

I myself spotted that issue as well a few days ago and got in touch with our SDK team to discuss this in more detail. I just now also shared your post :pray: I’ll share with you what they say about this and any changes we may make on the guidance.

Thank you for using Auth0 and it’s pretty cool that you are writing an npm library to make it easier for your company to use our service :muscle:

Hi Dan,

Thanks for the reply, and sorry for being so pissy when I wrote my comment, I was just tired and frustrated.

Let me know what the SDK team says. While I think my solution “works”, its pretty dirty and confusing from a maintainability standpoint. I’m going to be looking into a more elegant solution today.

1 Like

No worries! I understand, I think most (read: all of us) devs have been there at some point before :sweat_smile:

I’ll let you know what my teammates say about the authentication flow in Angular. Quick side question on that: when you login to your Angular application using Auth0, do you get a flash of the home page or whatever page where the authentication process was started and then it renders the protected page?

I haven’t noticed anything like that, but I’m also not following the guide when it comes to the implementation of the localAuthSetup and handleAuthCallback functions. I believe the way its currently set up means you can’t predict when the service is actually instantiated. Angular treeshakes services that are provided but not injected, so those 2 functions in the constructor aren’t going to run until the first component that needs the auth service requests it from the DI injector. That might be interrupting your flow and causing the flashing.

Just for my sanity’s sake I left localAuthSetup in the app.component (which means the auth0 service is always instantiated at the exact same point in the life cycle) and the handleAuthCallback function in my callback component located at the route “/callback”.

1 Like

:100: That’s how I feel I want to approach it too!

I’m doing some testing now, but I think this is going into version 2.0.0 of our @sigao/ng-auth0 library.

I abandoned the observable public properties on the service in favor of observable factory functions. Also found a pretty elegant way to ensure that the “isAuthenticated” stream is fresh both on initial subscription as well as able to accept pushed values from the handleAuthCallback response (which solves the initial problem).

the “_auth0ClientStream” variable is set up exactly like the docs with the “shareReplay” thing.

  public getAuth0Client(): Observable<Auth0Client> {
    return this._auth0ClientStream;
  }

  public getHandleRedirectCallback(): Observable<RedirectLoginResult> {
    return this.getAuth0Client()
      .pipe(concatMap((client: Auth0Client) => from(client.handleRedirectCallback())));
  }

  public getUserProfile(options?: GetUserOptions): Observable<any> {
    return this.getAuth0Client().pipe(
      concatMap((client: Auth0Client) => from(client.getUser(options))),
      tap(user => this.userProfile = user)
    );
  }

  public getIdTokenClaims(options?: GetIdTokenClaimsOptions): Observable<any> {
    return this.getAuth0Client().pipe(
      concatMap((client: Auth0Client) => from(client.getIdTokenClaims(options))),
      tap(claims => this.idTokenClaims = claims)
    );
  }

  public getTokenSilently(options?): Observable<string> {
    return this.getAuth0Client().pipe(
      concatMap((client: Auth0Client) => from(client.getTokenSilently(options))),
      tap(token => this.token = token)
    );
  }

  public getIsAuthenticated(): Observable<boolean> {
    return this.refreshAuthentication()
      .pipe(concatMap(() => this._isAuthenticatedStream));
  }

  private refreshAuthentication(): Observable<boolean> {
    return this.getAuth0Client()
      .pipe(
        concatMap((client: Auth0Client) => from(client.isAuthenticated())),
        tap(val => this._isAuthenticatedSubject.next(val))
      );
  }

Hopefully this helps any future users.