0

I use Auth0 from the latest Auth0 Angular SDK (v.1.8.2.) with Angular 13 and rxjs 7.4.0, and want to do the following:

  • if authorized, always fetch profile data from the user globally
  • have a guard which checks the profile data on some pages

I use the Auth0 Guard along with a custom Profile Guard for checking the profile data.

Problem: If I refresh the page, my Auth0 Guard would return true but my Profile Guard would always return false as my app did not yet fetch the profile data.


export class AuthService {
  profile: BehaviorSubject<Profile| undefined> = new BehaviorSubject<Profile| undefined>(undefined);
  public readonly profile$: Observable<Profile| undefined> = this.profile.asObservable();

  constructor(
    @Inject(DOCUMENT) private doc: Document,
    public auth0: Auth0Service,
    private httpHandler: HttpHandlerService
  ) {
    this.auth0.isAuthenticated$.subscribe((isAuth) => {
      if (isAuth) {
            this.httpHandler.fetchProfile().subscribe(
               (profile: Profile) => this.profile.next(profile))
      }
    })
  }
}

export class ProfileGuard implements CanActivate, CanActivateChild {

  constructor(private auth: AuthService, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true | UrlTree {
    return this.checkProfile(state.url);
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): true | UrlTree {
    return this.checkProfile(childRoute, state);
  }

  checkLogin(url: string): true | UrlTree {
    //how "wait" for the auth service to finish fetching the profile data?
    if (this.auth.profile.value?.isBaned!== true)
      return true;

    return this.router.parseUrl('/banned');
  }

}

In my app routing, I use both the Auth0 Guard and my custom guard as follows:

const routes: Routes = [
  { path: 'myProtectedRoute', component: UserComponent, canActivate: [Auth0Guard, ProfileGuard], runGuardsAndResolvers: 'always' }
]

How can I rewrite my profile guard to have it wait for the profile data to be fetched before proceeding?

Thanks in advance

1 Answers1

0
  1. You're trying to use a asynchronous variable synchronously which isn't possible. because your app doesn't know when the isAuthenticated$ would resolve. So you'd need to forward the async behavior to the canActivate.

  2. Avoid nested subscriptions which might lead to potential memory leaks. Use a higher order mapping operator like switchMap instead. See here for more info. You could also use RxJS function iif function to return an observable conditionally.

  3. Use RxJS map operator to transform the emission from an observable and return a different data.

import { Observable, iif, of } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

export class AuthService {
  public readonly profile$: Observable<boolean | Profile>;

  constructor(
    @Inject(DOCUMENT) private doc: Document,
    public auth0: Auth0Service,
    private httpHandler: HttpHandlerService
  ) {
    this.profile$ = this.auth0.isAuthenticated$.pipe(
      switchMap((isAuth) => 
        iif(
          () => isAuth,
          this.httpHandler.fetchProfile().pipe(
            map((profile: any) => <Profile>(profile))
          ),
          of(false)
        )
      )
    );
  }
}

export class ProfileGuard implements CanActivate, CanActivateChild {
  constructor(private auth: AuthService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
    return this.checkProfile(state.url);
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<true | UrlTree> {
    return this.checkProfile(childRoute, state);
  }

  checkLogin(url: string): Observable<boolean | UrlTree> {
    return this.auth.profile$.pipe(
      map((profile: boolean | Profile) =>
        !!profile
          ? profile?.isBaned
            ? this.router.parseUrl('/banned')
            : true
          : false
      )
    );
  }
}
ruth
  • 29,535
  • 4
  • 30
  • 57
  • Thank you for the advises! Unfortunately this does not work for me, when refreshing I will always receive "false" instead of data. Apparently `() => isAuth,` is always called –  Jan 26 '22 at 15:29
  • 1
    @TeaCup: `() => isAuth` will always be triggered. That is how the `iif` works. The thing to notice is if `isAuth` contain `true` or `false`. If `isAuth` is `true`, then `fetchProfile()` would be triggered, if not `of(false)` will be returned. So what does the `isAuth` contain? – ruth Jan 26 '22 at 15:34
  • Thanks for explaining! Makes perfect sense now! The solution works on regular flow but does not fetch new profile data reliably when the auth changes;/ apparently I will only fetch new profile data now if I am on a verification route –  Jan 26 '22 at 16:25
  • 1
    @TeaCup: I'm sorry. I don't quite understand this part. Based on your code, I'd assume the current profile information will be fetched (through `this.httpHandler.fetchProfile()`) each time the guard is triggered. – ruth Jan 26 '22 at 16:27
  • My profile is global and I need to update it all times when my subscription updates. The verified guard just needs to check the profile data, but wait for the profile if it is just being fetched. I was thinking of using a ReplaySubject perhaps but it does not work also;/ –  Jan 26 '22 at 16:33
  • 1
    @TeaCup: Could you show how `fetchProfile()` is defined? – ruth Jan 26 '22 at 16:36
  • sure, fetchprofile is supersimple:D `fetchProfile(): Observable { return this.http.post(url, this.httpOptions); }` –  Jan 26 '22 at 20:28