Using BehaviorSubject in auth-guard's canActivate
Asked Answered
R

1

10

What I want to achieve: I want to share authentication state across my application using BehaviorSubject. I use the authentication state e.g. inside an auth-guard to prevent the user from visiting login/register pages when the user already is authenticated.

Problem: because the BehaviorSubject has a initial value, which is false (not logged in), it seems that the auth-guard takes this first value, instead of waiting for the uid-sync.

AuthInfo (Auth state store):

export class AuthInfo {

  constructor(public uid: string) {}

  isLoggedIn() {
    return !!this.uid;
  }
}

AuthService:

@Injectable()
export class AuthService {

  static UNKNOWN_USER = new AuthInfo(null);
  authInfo$: BehaviorSubject<AuthInfo> = new BehaviorSubject<AuthInfo>(AuthService.UNKNOWN_USER);

  constructor(private af: AngularFire) {
    this.af.auth.subscribe(auth => {
      if (auth) {
        console.log('got the uid');
        this.authInfo$.next(new AuthInfo(auth.uid));
      } else {
        this.authInfo$.next(AuthService.UNKNOWN_USER);
      }
    });
  }

  logIn(email: string, password: string): Promise<FirebaseAuthState> {
    return this.af.auth.login({email: email, password: password});
  }
}

AuthGuard:

@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate(): Observable<boolean> {
    return this.authService.authInfo$.map(authInfo => {
      if (authInfo.isLoggedIn()) {
        this.router.navigate(['/user'])
      }
      return !authInfo.isLoggedIn();
    });
  }
}

So canActivate is processed with authInfo.isLoggedIn() being false and after a fraction of a second I see Got the uid in the console. Any ideas how to prevent the first false? I think that BehaviorSubject is correctly used here, because it allows us to set an initial state. However the auth-guard will always receive false (the initial value). Right after that the

this.authInfo$.next(new AuthInfo(auth.uid));

will trigger, when the canActivate method was already finished.

Retroflex answered 21/2, 2017 at 11:40 Comment(0)
D
14

Guard's canActivate method, as its name suggests, resolves upon the attemption to activate specific route.

As I understand from the code provided, you're trying to redirect user to the /user route upon retrieving auth uid from the server. To achieve that, you need to initiate redirecting to the desired route once the auth uid is retrieved - e.g. after logging in, and let your guard do its job, enable or deny the access to the route.

After sorting things out, here is the walk-through the changed code and structure:

AuthInfo class:

// No changes.

AuthService:

@Injectable()
export class AuthService {

  static UNKNOWN_USER = new AuthInfo(null);
  authInfo$: BehaviorSubject<AuthInfo> = new BehaviorSubject<AuthInfo>(AuthService.UNKNOWN_USER);

  constructor(private af: AngularFire) { }

  logIn(email: string, password: string): Promise<FirebaseAuthState> {
    return this.af.auth.login({email: email, password: password});
  }

  getAuthInfo(): Observable<AuthInfo> {
    return this.af.auth.map(auth => {
      if(auth) {
        console.log('got the uid');
        let authInfo = new AuthInfo(auth.uid);
        this.authInfo$.next(authInfo);
        return authInfo;
      } 
      else {
        this.authInfo$.next(AuthService.UNKNOWN_USER);
        return AuthService.UNKNOWN_USER;
      }
    });
  }
}

AuthGuard:

@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate(): Observable<boolean> | boolean {

    // get the most recent value BehaviorSubject holds
    if (this.authService.authInfo$.getValue().isLoggedIn()) {
      // can access targeted route
      return true;
    }

    /*
    User is not logged in as stored authInfo indicates, 
    but in case the page has been reloaded, the stored value is lost, 
    and in order to get real auth status we will perform the server call,
    (authService.getAuthInfo method will automatically update the BehaviorSubject value, 
    and next time the protected route is accessed, no additional call will be made - until 
    the next reloading).
    */

    return this.authService.getAuthInfo()
        .map((authInfo: AuthInfo) => {
          if(authInfo.isLoggedIn()) {
            // can access targeted route
            return true;
          }

          this.router.navigate(['login']); // redirect to login screen
          return false;
        });
  }
}
Didactic answered 24/2, 2017 at 1:17 Comment(7)
first let me thank your for the answer. I have a couple of questions. Following your suggestion I would have to inject the router into the authentication service which I think is a bad practice. The auth service should not redirect. By using the auth guard I want to prevent the user from navigating to the login or register route when he is already logged in. So this is a valid use case for using an auth guard, right? You say I should use getValue on the BehaviorSubject. Can you think of a use case where I would subscribe to it?Retroflex
Hi, 1) My intention of suggesting that was only to point out that Guard doesn't listen for BehaviorSubject changes, it resolves only upon the attemption of activating guarded route. You can redirect from wherever you want. 2) Regarding the preventing user for navigating to login or register routes, you should create separate guard with the reversed logic called something like: 'notAuthGuard'. 3) You're subscribing to it where you want to listen for its changes - Guard simply isn't supposed to do that. Now when I have a clear understanding of the problem, I will try to complement my answer.Didactic
Okay I got that, thank you. But think about following use case. The user is logged in because there is a user token in local storage. Say we access the storage in a async manner using observables. So retrieving the token can take some time. When I use getValue I will get no token as return value, when the user refreshes the page. The BehaviourSubject will emit the value when it arrives, but canActivate was already called with the inital value, which is "no token". So the user will see the login page, but he should not. So maybe BehaviorSubject is not the right tool here?Retroflex
But I like the idea of the BehaviorSubject having an initial value "not logged in" and later "logged in".Retroflex
@zabware I've changed your code a little bit, and updated the answer. The case you're worried about is also covered. Please take the attentive look. ThxDidactic
Thanks that definitely answers my questionRetroflex
Even tho it's 2020, this answer saved so much time for me. Thank you! Just code update tip for who needs this: - When using .map make sure to use it as .pipe(map()..) - auth has been changed to firebaseService.authState instead of just authCobweb

© 2022 - 2024 — McMap. All rights reserved.