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.