auth.user$ fires twice on page refresh or when returned from universal login
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
Thanks, will have a look and see what could be causing this. Are you perhaps using LocalStorage?
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,
}
],
},
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?
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
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 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 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))
);
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)
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.
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 ?
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.
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
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, thenauth.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).
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.
Any update on that bug report?
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.
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.
Closed by #367 , will be released with the beta version of v2 of this SDK in the next few weeks.