1
 src
  |_auth/
    |_authentication/
    |_auth-service.ts
    |_auth-guard.ts
    |_is-logged-guard.ts
  |_dashboard/

auth-guard-service.ts

export class AuthService {
  public user: Observable<firebase.User>;
  public userDetails: firebase.User = null;
  public userProfileRef: firebase.database.Reference;
  userData: any[] = [];
  constructor(private _firebaseAuth: AngularFireAuth, private router: Router) {
    this.user = _firebaseAuth.authState;
    this.userProfileRef = firebase.database().ref('/userProfile');
        this.user.subscribe(
      (user) => {
        if (user) {
          this.userDetails = user;
        } else {
          this.userDetails = null;
        }
      }
    );
  }

  isLoggedIn() {
    if (this.userDetails == null) {
      return false;
    } else {
      return true;
    }
  }

  doSignOut() {
    this._firebaseAuth.auth.signOut()
      .then((res) => this.router.navigate(['/auth/login']));
  }
}


auth-guard.ts

@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate() {
    return this.auth.user.take(1).map(authState => !!authState).do(authenticated => { new Promise<boolean>( (resolve, reject) => {
      if (!authenticated) {
        this.router.navigate(['auth/sigin']);
        return resolve(false);
      } else {
        return resolve(true);
      }
  }

}

is-logged-guard.ts - I know this is the problem. How will I fix it?

@Injectable()
export class IsLoggedGuard implements CanActivate {

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

  canActivate() {
    return !this.auth.isLoggedIn();
  }

}

app-routing.module.ts


const routes: Routes = [
  { path: 'dashboard',
    canActivate: [AuthGuard],
    loadChildren: './dashboard/dashboard.module#DashboardModule'
  },
  {
    path: 'auth',
    component: NbAuthComponent,
    canActivate: [IsLoggedGuard],
    children: [
      {
        path: '',
        component: SignInComponent,
      },
      {
        path: 'SignIn',
        component: SignInComponent,
      },
      {
        path: 'SignUp',
        component: SignUpComponent,
      },
    ],
  },
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  { path: '**', redirectTo: 'dashboard' },
];

const config: ExtraOptions = {
  useHash: true,
};

@NgModule({
  imports: [RouterModule.forRoot(routes, config)],
  exports: [RouterModule],
})
export class AppRoutingModule {
}

Case 1: User not logged in

No problem. Auth guard protects the dashboard from unauthenticated users and will redirect them to the auth pages (i.e. login page).

Case 2: User already logged in #

No problem. If the authenticated users access the dashboard via localhost:4200 or localhost:4200/#/dashboard or localhost:4200/#/ or localhost:4200/#/RANDOM_INVALID_URL it all work. The guard will also prevent authenticated users who is already inside the dashboard from accessing the authentication pages.

Case 3: User already logged in

Problem. If the authenticated users access the dashboard via localhost:4200/#/auth or localhost:4200/#/auth/signin the guard will fail to protect and redirect the user to the dashboard home page. (I.e. John is already logged in and open up a new Chrome tab, and entered localhost:4200/#/auth the guard will not prevent him from accessing it). How can I fix my guard to prevent John from accessing the authentication pages if he is already authenticated?

2 Answers 2

5

You should change your IsLoggedGuard like this:

@Injectable()
export class IsLoggedGuard implements CanActivate {

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

  canActivate() {
    return this.auth.user
                    .take(1)
                    .map(authState => {
                       if (authState) {
                          //user is already loggedin
                          //route the user to Dashboard page
                          //Or a page where you want the app to naviagte
                          this.router.navigate("dashboard route");
                          //dont show the Login page
                          return false;
                       } else {
                         //user is not loggedin
                         return true;
                       }
                    });
  }

}

You were seeing the issue because when you enter the "localhost:4200/#/auth" url in the browser, then your AuthGuard.user.subscribe [i.e. in the constructor this.user.subscribe(] might not have emitted any value yet when IsLoggedGuard's canActivate() executed [i.e. AuthService.isLoggedIn() may return false because subscribe callback might not have executed (which fills the userDetails)].

Let me know if it solves your problem.

There could be a better way to implement your AuthService as well as Guards to make use of AuthService. Let me know if you want a better code.

EDIT - Another approach to write AuthService

Let's change the AuthService like this:

export class AuthService {

    //NOTE: I AM JUST SHOWING TWO THINGS - isUserLoggedIn AND userDetails
    //FROM THIS CODE YOU WILL GET AN IDEA HOW TO WRITE OTHER PROPERTIES WHICH ARE RELEVANT FOR YOUR APP

    //This will be used as a source for various observables
    private _authState$: Observable<any>;

    //Have an observable which will tell if user is loggedin or not
    isUserLoggedIn$: Observable<boolean>;
    userDetails$: Observable<firebase.User>;

    public userProfileRef: firebase.database.Reference;

    constructor(private _firebaseAuth: AngularFireAuth, private router: Router) {            
      this.userProfileRef = firebase.database().ref('/userProfile');
      this.setupObserables();
    }

    setupObserables() {

        // this observable will broadcast the emited values to multiple subscribers [or composed/dependent observables]
        this._authState$ = this._firebaseAuth.authState
                                        .publishReplay(1)
                                        .refCount();

        // lets componse/derive different observables required by the consumer of this service

        // This observable's emitted value will tell if user is logged in or not
        this.isUserLoggedIn$ = this._authState$
                                   .map(user => {
                                        return user ? true : false;
                                    });

        // This observable's emited value will return the user's detail [NOTE If user is not logged in then the emitted value will be NULL
        // i.e. userDetail is NULL; Your consumer of this observable should decide what to do with NULL/NOT NULL Value]        
        this.userDetails$ = this._authState$
                                .map(user => user);
    }    

    doSignOut() {
      this._firebaseAuth.auth.signOut()
        .then((res) => this.router.navigate(['/auth/login']));
    }
  }

Now let's make use of updated AuthService in IsLoggedGuard:

    @Injectable()
    export class IsLoggedGuard implements CanActivate {

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

      canActivate() {
        return this.auth.isUserLoggedIn$
                        .take(1)
                        .map(isLoggedIn => {
                           if (isLoggedIn) {
                              //user is already loggedin
                              //route the user to Dashboard page
                              //Or a page where you want the app to naviagte
                              this.router.navigate("dashboard route");
                              //dont show the Login page
                              return false;
                           } else {
                             //user is not loggedin
                             return true;
                           }
                        });
      }

    }

Now let's make use of updated AuthService in AuthGuard:

    @Injectable()
    export class AuthGuard implements CanActivate {

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

    canActivate() {
        return this.auth.isUserLoggedIn$
                   .take(1)
                   .map(isLoggedIn => {
                    if (!isLoggedIn) {
                       //user isNOT loggedin
                       //route the user to login page
                       this.router.navigate(['auth/sigin']);
                       //dont show the next route
                       //lets fail the guard
                       return false;
                    } else {
                      //user is loggedin; pass the guard i.e. show the next route associated with this guard
                      return true;
                    }
                 });
        }

    }

Now suppose some component (assume the component name is UserComponent) you want to show the logged in user detail:

....component decorator...
export class UserComponent implements OnInit {

    userDetails$: Observable<User>;
    constructor(private _authService: AuthService) {
        this.userDetails$ = this._authService.userDetails$;
    }
}

Render the userDetails like this:

<div *ngIf="(userDetails$ | async) as userDetails">
    <!-- Render your user details here -->
    <!-- If userDetails is NULL then nothing will be rendered -->
</div>

THINGS TO NOTE - In this updated code NOWHERE we are subscribing to any of the observables. Notice async in the component template, this takes care of subscribing/unsubscribing of the used observable.

Hope it will give you a direction/idea. Let's be "Reactive" as much as possible instead of "Imperative"..:)

NOTE: I have tested the equivalent code in rxjs6. Looks like you are using rxjs5 so I have adjusted the posted code as per rxjs5. Hope it will work.

Sign up to request clarification or add additional context in comments.

2 Comments

This is awesome. Thank you so much for teaching me the correct way of doing it. If you would be so kind to teach me the better way of doing AuthService.
Really appreciate your time. Thank you!
0

I had login and signup page that should not load if user already logged in so in that case i put check in both component ngOnInit method as below.

ngOnInit() {
    if(localStorage.getItem('isLoggedIn')){
        this.router.navigate(['/']);
        return;
    }
}

Here is my routes configuration

const routes: Routes = [
    { path: '', loadChildren: './modules/dashboard/dashboard.module#DashboardModule', canActivate: [AuthGuard] },
    { path: 'auth', loadChildren: './modules/auth/auth.module#AuthModule' },
    { path: '**', component: NotFoundComponent }
];

If you have many routes that doesn't require authentication so instead of above create another guard for it (Not same that created for authenticated routes) and do check there if user logged in than redirect to default route of authenticated routes.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.