Search code examples
angularauthenticationpersistenceangular-router-guards

Angular AuthGuard and login persistence: Router automatically redirects to non-truthy route even if logged in


As the title suggests, I want to protect routes via canLoad. This works to some extent. However, I have two issues with the code.

Here are my code snippets

AppComponent

ngOnInit(): void {
    this.authServ.reauthenticate().subscribe()
}

AuthService

user = new BehaviorSubject<User>(null as any);
reauthenticate(): Observable<User> {
    // return HTTP call here and pipe the user data into user BehaviorSubject
}

AuthGuard

canLoad(){
      return this.authServ.user.pipe(
        map((user) => {
          if (!!user?._token) {
            return true;
          }
          this.router.navigate(['/login']);
          return false;
        })
      );
}

As you can guess, the router can access the user but still redirects to the /login path. Knowing it doesn't work, I brute-forced my way, making two HTTP calls to the server (which is generally bad, I know) to trick the system into acknowledging a persistent user. Here is the code snippet.

AuthGuard

canLoad() {
      return this.authServ.user.pipe(
        switchMap((user) => {
          if (!!user && !!user._token) {
            return of(user);
          }
          return this.authServ.reauthenticate();
        }),
        map((user) => {
          if (!!user?._token) {
            return true;
          }
          this.router.navigate(['/login']);
          return false;
        })
      );
}

So my question boils down to either of these two things:

  1. How can I make sure that I only call the re-tokenizer endpoint only once but still not continue to not be routed upon refresh?
  2. How can I make the BehaviorSubject work until I receive a user object?

I have checked the following links: AuthGuard router angular and What is the difference between Subject and BehaviorSubject?. However, upon using a Subject, I may need to reauthenticate the user every time I visit a guarded route which is counterintuitive.


Solution

  • Okay, so I've solved this problem. What I did was to create a new Subject at the AuthService. The guard checks if the BehaviorSubject inside the AuthService returns a null, and if it does, I will switchMap into using the Subject. Here is the code.

    AuthGuard

    canLoad() {
        return this.authServ.user.pipe(
            switchMap((user) => {
              if (!!user && !!user._token) {
                return of(user);
              }
              return this.authServ.initialLogin;
            }),
            map((user) => {
              if (!!user?._token) {
                return true;
              }
              this.router.navigate(['/login']);
              return false;
            })
    }
    

    AuthService

    user = new BehaviorSubject<User>(null as any);
    initialLogin = new Subject<User>();
    reauthenticate(): Observable<User> {
        // return HTTP call here and pipe the user data into user BehaviorSubject
        return this.http.get<User>(URL, {withCredentials: true}).pipe(tap((user) => {
            this.initialLogin.next(user);
            this.user.next(user);
        }))
    }
    

    AppComponent

    ngOnInit(): void {
        this.authServ.reauthenticate().subscribe()
    }
    

    This way, I only authenticate once upon the start of the project