Cannot distinguish between not authenticated and initial loading state
Describe the bug Cannot distinguish between not authenticated and initial loading state.
isAuthenticated$ emits DEFAULT_AUTHRESULT on post login redirect, and a few frames later emits a successful AuthenticatedResult (isAuthenticated is true). Because of this, if I place a call to .authorize() when isAuthenticated$ is fired with DEFAULT_AUTHRESULT on startup, I will get a login loop with the IDP. Need a way to distinguish between DEFAULT_AUTHRESULT which is the initial loading state, and actual token not present or token expired.
To Reproduce Steps to reproduce the behavior:
- login to IDP
- IDP redirects to angular app
- app.component ctor runs, the remaining steps happen in the ctor
- this.oidcSecurityService.isAuthenticated$.subscribe(result => console.log(result.isAuthenticated)); //first time always as false after post login redirect
- This is the problem point. The authenticatedInternal$ BehaviorSubject emits its inital value which is the same as token not present/token expired. Here my logic would think the user is not logged an and redirect to IDP login page. If I remove this logic then we can continue on to step 6.
- this.oidcSecurityService.isAuthenticated$.subscribe(result => console.log(result.isAuthenticated)); //fires as true a few frames later
- this.oidcSecurityService.checkAuth().subscribe((loginResponse: LoginResponse) => { console.log(loginResponse.isAuthenticated)}); //fires as true
- If F5 to refresh browser then this.oidcSecurityService.isAuthenticated$.subscribe(result => console.log(result.isAuthenticated)); // fires as true
- and a few frames later this.oidcSecurityService.isAuthenticated$.subscribe(result => console.log(result.isAuthenticated)); //fires as true
Expected behavior the default value of this.authenticatedInternal$ BehaviorSubject in (looking at compiled output)angular-auth-oidc-client.js would ideally be something other than what is emitted when (there is no token present, or when the users current token expires) so that we can ignore the initial loading state and thus handle token not exists/token expired scenarios appropriately.
Desktop (please complete the following information):
- OS: Windows 10 Enterprise build 19043.1237
- Browser: Chrome
- Version 94.0.4606.61 (64-bit)
this works as temporary fix
(this.oidcSecurityService as any).authStateService.authenticatedInternal$.next(null);
same thing for userService.userDataInternal$
Hey, thanks for this. Would you expect null as initial value as a better choice here?
Yes, because rxjs BehaviorSubjects emit their default value, using null as the initial value instead of DEFAULT_AUTHRESULT and DEFAULT_USERRESULT, would allow us to differentiate from the initial BehaviorSubjects value vs a not logged in/expired value.
using this at the top of app.componet.ts ctor as a temporary fix works great.
(this.oidcSecurityService as any).authStateService.authenticatedInternal$.next(null);
(this.oidcSecurityService as any).userService.userDataInternal$.next(null);
if null is not preferred, then perhaps something that relays more intent, but is still different from a not logged in/expired value.
This is just a suggestion but usually in the rxjs world, such an observable would not emit with a default initial value but instead emit only once a first value is emitted. That way, subscribers get notified only when there is an actual result and don't have to have knowledge of this placeholder initial value and handling skipping that value.
This is can be archieved by using a ReplaySubject of size 1 (new ReplaySubject(1)), which would have the same behavior as a BehaviorSubject except for the first placeholder value.
Hey @Samuel-B-D , thanks for the explanation and I totally agree with you. We had that implemented initially like this, but a user wanted to have a default value for any reason, so we had this changed long time ago.
Closing as there hasn't been any response from the issue author. Please re-open if you are still seeing problems here.
I have experienced the same problem.
In my case, this was happening because there was an AuthGuard on redirectUrl.
Possible fix:
checkAuth(): Observable<boolean | UrlTree> { return this.eventService.registerForEvents().pipe( filter((event) => event.type != EventTypes.CheckingAuth), switchMap(() => this.oidcSecurityService.isAuthenticated$)).pipe( map(({ isAuthenticated }) => { if (isAuthenticated) { return true; } return this.router.parseUrl('/unauthorized'); }) ); }