Angular 8 isAuthenticated race condition

So after taking a step back and examining how the suggested code runs I am kind of curious how this was working as intended for the person who set this up for an Angular application. I noticed a few anti-patterns when it comes to RxJs and some assumptions made around the whole asynchronous process of how these things would happen.

This is what I ended up with:

auth.service.ts:

@Injectable({ providedIn: 'root' })
export class AuthService {
  refreshSub: Subscription;

  auth0Client$: Observable<Auth0Client> = from(
    createAuth0Client({
     domain: environment.auth0.domain,
     client_id: environment.auth0.client_id,
     redirect_uri: environment.auth0.redirect_uri,
     audience: environment.auth0.audience
   })).pipe(shareReplay(1), catchError(err => throwError(err)));

  isAuthenticated$ = this.auth0Client$.pipe(concatMap((client: Auth0Client) => 
    from(client.isAuthenticated())));

  handleRedirectCallback$ = this.auth0Client$.pipe(
    concatMap((client: Auth0Client) => from(client.handleRedirectCallback())));

  constructor(public router: Router) {}

  getTokenSilently$(options?): Observable<string> {
    return this.auth0Client$.pipe(concatMap((client: Auth0Client) => 
      from(client.getTokenSilently(options))));
  }

  login(redirectPath: string = '/home/dashboard'): Observable<void> {
    return this.auth0Client$.pipe(
      concatMap((client: Auth0Client) =>
        client.loginWithRedirect({
        redirect_uri: environment.auth0.redirect_uri,
        appState: { target: redirectPath }
      })));
  }

  handleAuthCallback(): Observable<{ loggedIn: boolean; targetUrl: string }> {
     return of(window.location.search).pipe(
      concatMap(params =>
        iif(() => params.includes('code=') && params.includes('state='),
           this.handleRedirectCallback$.pipe(concatMap(cbRes =>
              this.isAuthenticated$.pipe(take(1),
                map(loggedIn => ({ loggedIn,
              targetUrl: cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/'
            }))))),
          this.isAuthenticated$.pipe(take(1), map(loggedIn => ({ loggedIn, targetUrl: null }))))));
  }

  logout() {
   this.auth0Client$.subscribe((client: Auth0Client) => {
     client.logout({
       client_id: environment.auth0.client_id,
       returnTo: environment.auth0.logout_url
    });
  });
 }
}

auth.guard.ts

canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
     return this.authService.isAuthenticated$.pipe(
       concatMap(_ => this.authService.handleAuthCallback()),
       concatMap(result => iif(() => result.loggedIn, of(true), this.authService.login(state.url).pipe(map(_ 
=> false)))));
 }

home.component.ts

  ngAfterViewInit(): void {
    this.location.replaceState('/home/dashboard');
  }

This works for me now. I made it this way to enforce the AuthGuard to wait until the checks are complete and not making assumptions that things will complete before the AuthGuard runs. Also, I changed some of the RxJs Code to use concatMap instead of .subscribe and to return Observables instead of subscribing to new ones inside of function calls like login(). In general, you should avoid calling subscribe within subscriptions to avoid memory leak headaches from having long-lived subscriptions that don’t ever complete.

Hope this helps anyone who runs into this issue.

11 Likes