auth0-angular icon indicating copy to clipboard operation
auth0-angular copied to clipboard

auth.user$ fires twice on page refresh or when returned from universal login

Open Teacup5000 opened this issue 3 years ago • 17 comments

Describe the problem

The auth.user$ is called twice on page refresh (or when returning from universal login), while other similar observable such as auth.isAuthenticated$ do not trigger twice. The returned user-object is the same for both calls.

What was the expected behavior?

auth.user$ shall only be called once after page refresh/login, similar to auth.isAuthenticated$

Reproduction

Have a setup similar to the demo app, with an auth service constructor as follows:

  constructor(
    @Inject(DOCUMENT) public doc: Document,
    private auth: AuthService
  ) {
    this.auth.isAuthenticated$.subscribe((isAuth) => {
      console.log(isAuth) //this is called only once on page refresh or return from universal login
    })
    this.auth.user$.subscribe((user) => {
      console.log(user) //this is called twice and **the same object** is returned on page refresh or return from universal login
    });
  }

Environment

  • Angular 13
  • FF/Chrome
  • auth0 angular sdk 1.9.0

Teacup5000 avatar Feb 16 '22 11:02 Teacup5000

Thanks, will have a look and see what could be causing this. Are you perhaps using LocalStorage?

frederikprijck avatar Feb 16 '22 11:02 frederikprijck

Yes I do, here is my full environment config:

  auth: {
    domain: 'removed',
    clientId: 'removed',
    audience: 'removed',
    redirectUri: window.location.origin,
    cacheLocation: 'localstorage' as 'localstorage',
    useRefreshTokens: true,
    scope: 'offline_access',
    errorPath: '/autherr'
  },
  httpInterceptor: {
    allowedList: [
      {
        uri: 'removed',
        allowAnonymous: true,
      }
    ],
  },

Teacup5000 avatar Feb 16 '22 11:02 Teacup5000

I haven't been able to reproduce this, I seem to be getting a single log on page-load in both cases. I assume something else is causing this, would you be able to share a reproduction using our Quickstart Sample?

frederikprijck avatar Feb 16 '22 13:02 frederikprijck

We are also seeing this issue.

Our current workaround is to use distinctUntilChanged

this.authService.user$.pipe(
    distinctUntilChanged((prev, curr) => _.isEqual(prev, curr))
).subscribe((user) => {
    console.log(user);
})

Edit: Also not sure if it's related or not, in some cases using the async pipe in HTML on user$ causes an infinite loop. <app-x [user]="getUser() | async"></app-x> where getUser returns the observable

getUser() {
    return this.authService.user$.pipe(
        distinctUntilChanged((prev, curr) => _.isEqual(prev, curr))
    );
}

Edit2: replaced prev.sub === curr.sub with _.isEqual(prev, curr) as it did not work correctly when the user object changed

danmana avatar Feb 16 '22 13:02 danmana

Please know that using the sub in your distinctUntilChanged won't ever update the user, which could be needed when changes are being made to your user account while using the app.

The issue seems to be different. Feel free to open another issue and I can have a look @danmana

frederikprijck avatar Feb 16 '22 14:02 frederikprijck

@frederikprijck sorry, I oversimplified our actual code ... we don't really check on the user.sub In our case we only care about a few fields from the user, so we use a map() and then do a distinctUntilChanged on the resulting object (comparing all fields)...

The infinite calls using async pipe is most likely something else, but the double trigger of user$ observable seems very similar

danmana avatar Feb 16 '22 14:02 danmana

@danmana Regardless, I can't reproduce that behavior either using the following:

{{getUser() | async | json}}

getUser() {
  return this.authService.user$;
}

Unrelated, but keep in mind it's not recommended to call methods from the HTML in Angular, you would typically use:

<app-x [user]="user$ | async"></app-x>

user$ = this.authService.user$.pipe(
   distinctUntilChanged((prev, curr) => _.isEqual(prev, curr))
);

frederikprijck avatar Feb 16 '22 15:02 frederikprijck

I'll try to reproduce this in a clean project. Meanwhile, I added some console.logs in our code as well as auth0-angular (a tap at the end of each observable from AuthState - see isLoading$, accessTokenTrigger$, isAuthenticatedTrigger$, isAuthenticated$, user$)

my-component

ngOnInit(): void {
      console.log('my-component.ngOnInit');
      this.myService.getUserInfo()
        .subscribe(info => {
          console.log('my-component.ngOnInit.subscribe');
          // do stuff with info
        });
  }

my-service

  public getUserInfo(): Observable<any> {
    console.log('my-service.getUserInfo');
    return this.authService.user$.pipe(,
      tap(user => console.log('my-service.getUserInfo.pipe.tap', user)),
      map(user => {
        return {
           // some fields from user
        };
      }));
  }

Here is an example when refreshing a page (removed any private info):

// this is part of the page refresh (checking existing session)
auth0-auth0-angular.js:175 isLoading$ true
auth0-auth0-angular.js:175 isLoading$ false
auth0-auth0-angular.js:198 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:203 isAuthenticated$ true
auth0-auth0-angular.js:185 accessTokenTrigger$ {previous: null, current: 'zzz'}
auth0-auth0-angular.js:198 isAuthenticatedTrigger$ true


// our component gets initialized and subscribes to user$
my-component.ts:43 my-component.ngOnInit
my-service.ts:61 my-service.getUserInfo
auth0-auth0-angular.js:175 isLoading$ false
auth0-auth0-angular.js:185 accessTokenTrigger$ {previous: null, current: 'zzz'}
auth0-auth0-angular.js:198 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:198 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:207 user$ {…}
my-service.ts:63 my-service.getUserInfo.pipe.tap {…}
my-component.ts:46 my-component.ngOnInit.subscribe
auth0-auth0-angular.js:207 user$ {…}
my-service.ts:63 my-service.getUserInfo.pipe.tap {…}
my-component.ts:46 my-component.ngOnInit.subscribe

As you can see there is only one subscription being made, but isAuthenticatedTrigger$ is triggered twice, which triggers user$ twice (with the exact same user info)

danmana avatar Feb 17 '22 11:02 danmana

The fact that you only have one subscription doesn't exclude something else in your code can trigger another emit (e.g. calling getAccessTokenSilently will emit a user).

Would be good if you can share an actual project, the code above is pretty much the same as I already mentioned that I tried and was unable to reproduce.

frederikprijck avatar Feb 17 '22 11:02 frederikprijck

You are right, there must be something else triggering another emit. I double-checked the code and there are no other uses of AuthService on this page (at least from our code).

However, I just realized that this is a private page protected with AuthGuard. I tested calling the same code from a public page, and I see only 1 user$ emit.

So I think it might be related to the guard. I added more logging to AuthGuard

Here are the logs from the private page

Angular is running in development mode. Call enableProdMode() to enable production mode.
auth0-auth0-angular.js:516 AuthGuard.canActivate called
auth0-auth0-angular.js:524 AuthGuard.redirectIfUnauthenticated called
auth0-auth0-angular.js:175 isLoading$ true
auth0-auth0-angular.js:175 isLoading$ false
auth0-auth0-angular.js:197 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:202 isAuthenticated$ true
auth0-auth0-angular.js:532 AuthGuard.redirectIfUnauthenticated.tap true
auth0-auth0-angular.js:185 accessTokenTrigger$ {previous: null, current: 'zzz'}
auth0-auth0-angular.js:197 isAuthenticatedTrigger$ true
my-component.ts:43 my-component.ngOnInit
my-service.ts:61 my-service.getUserInfo
auth0-auth0-angular.js:175 isLoading$ false
auth0-auth0-angular.js:185 accessTokenTrigger$ {previous: null, current: 'zzz'}
auth0-auth0-angular.js:197 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:197 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:206 user$ {…}
my-service.ts:63 my-service.getUserInfo.pipe.tap {…}
my-component.ts:46 my-component.ngOnInit.subscribe
auth0-auth0-angular.js:206 user$ {…}
my-service.ts:63 my-service.getUserInfo.pipe.tap {…}
my-component.ts:46 my-component.ngOnInit.subscribe

And here are the logs from the public page (using same service)

Angular is running in development mode. Call enableProdMode() to enable production mode.
landing.page.ts:116 my-public-component.ngOnInit
my-service.ts:61 my-service.getUserInfo
auth0-auth0-angular.js:175 isLoading$ true
auth0-auth0-angular.js:175 isLoading$ true
auth0-auth0-angular.js:175 isLoading$ true
3auth0-auth0-angular.js:175 isLoading$ false
auth0-auth0-angular.js:197 isAuthenticatedTrigger$ true
auth0-auth0-angular.js:206 user$ {…}
my-service.ts:63 my-service.getUserInfo.pipe.tap { …}
landing.page.ts:119 my-public-component.ngOnInit.subscribe

Can you reproduce it on a page with AuthGuard ?

danmana avatar Feb 17 '22 13:02 danmana

I can't reproduce it using a page that is protected by our Guard no.

If you could try and send me a reproduction, I am happy to look into it and see what's causing it.

frederikprijck avatar Feb 17 '22 13:02 frederikprijck

I was able to create a small project that reproduces the problem. - AuthHttpInterceptor was the bit that made it reproducible

https://github.com/danmana/auth0-double-trigger

First set your credentials in app.module.ts

domain: 'YOUR_DOMAIN',
      clientId: 'YOUR_CLIENT_ID',

Then open http://localhost:4200 and click login

After this click on Public Page - here user$ triggers once Click on Private Page - here user$ triggers twice

With only AuthGuard it did not reproduce. Then I tried with a lazy loaded module - still not reproducible. However, as soon as I added a http call and AuthHttpInterceptor it started reproducing.

I hope this helps tracking down the issue

danmana avatar Feb 17 '22 14:02 danmana

Can confirm this @danmana, looks like it's the AuthHttpInterceptor

Steps to reproduce:

  • Have a page with one or more http call(s) + activated AuthHttpInterceptor
  • Subscribes to auth0's auth.user$
  • Navigate to said page or refresh

Result:

  • AuthHttpInterceptor will fire auth.user$ for every http call made. I.e. navigate to a route with 4 x http calls, then auth.user$ will fire 4 x times
  • This happens even if not authenticated. If you're logged out, it will still fire for every http call (each time with null).

Teacup5000 avatar Feb 17 '22 15:02 Teacup5000

Thanks, I am able to see the behavior now. For now, probably best to work around this by using disctinctUntilChanged, providing it with a compare function.

I will look into how we can improve this.

frederikprijck avatar Feb 17 '22 16:02 frederikprijck

Any update on that bug report?

anschm avatar Jun 01 '22 11:06 anschm

I have looked into it and I am not sure it's something we can fix on the short term as it originates in our underlying SPA-JS SDK. I believe it's better it emits too much than too little, but yeah you can work around it as mentioned.

Not ideal, but definitely still on our radar so keeping this open.

frederikprijck avatar Jun 01 '22 12:06 frederikprijck

We have just shipped v2 beta of Auth0-SPA-JS. Once this is stable, we will start working at a v2 for our Angular SDK, which will incorporate v2 of SPA-JS.

v2 of SPA-JS should allow us to avoid the issue described here, even though we can never guarantee we won't ever have unnecessary emits due to the nature of the underlying SDK. I would advice to always add a distinctUntilChanged based on certain properties.

frederikprijck avatar Sep 30 '22 11:09 frederikprijck

Closed by #367 , will be released with the beta version of v2 of this SDK in the next few weeks.

frederikprijck avatar Nov 07 '22 15:11 frederikprijck