7
votes

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.

1

1 Answers

8
votes

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;
        });
  }
}