Angular 5 canActivate redirecting to login on browser refresh

22,717

The canActivate() method is called directly on page refresh. So it always returns false:

canActivate() {
  this.authService.authState.subscribe(state => {
    this.status = state.toString(); // This is called async/delayed.
  });
  // so method execution proceeds

  // isLoggedIn() returns false since the login stuff in AuthService.constructor
  // is also async:    .subscribe((user) => { /* delayed login */ });
  if(this.authService.isLoggedIn()) {
    return true;
  }

  // so it comes here
  this.router.navigate(['/']); // navigating to LoginComponent
  return false;                // and canActivate returns false
}

The solution:

import { CanActivate, Router, ActivatedRouteSnapshot,
         RouterStateSnapshot } from '@angular/router';

// ...

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
  // when the user is logged in and just navigated to another route...
  if (this.authService.isLoggedIn) { return true; } 

  // proceeds if not loggedIn or F5/page refresh 

  // Store the attempted URL for redirecting later
  this.authService.redirectUrl = state.url;

  // go login page
  this.router.navigate(['/']);
  return false;
}

now, back in the little changed AuthService: (only have changed/relevant code here)

export class AuthService {

  // new
  redirectUrl: string;

  // BehaviorSubjects have an initial value.
  // isLoggedIn is property (not function) now:
  isLoggedIn = new BehaviorSubject<boolean>(false);

  // params declared private and public in constructor become properties of the class
  constructor(private firebaseAuth: AngularFireAuth, private router: Router) {
    // so this.user is not required since it is reference to this.firebaseAuth
    this.firebaseAuth.authState.subscribe((user) => {
      if (user) {
        this.loggedIn.next(true);

        // NOW, when the callback from firebase came, and user is logged in,
        // we can navigate to the attempted URL (if exists)
        if(this.redirectUrl) {
          this.router.navigate([this.redirectUrl]);
        }
      } else {
        this.loggedIn.next(false);
      }
    }
  }

}

Note: I have written this code in the answer box and compiled it in my brain. So bugs may exist. Also I don't know if this is actually best practise. But the idea should be clear?!

Based on the Angular Routing Guide

Seems like there are similar problems/solutions out there: Angular 2 AuthGuard + Firebase Auth

Share:
22,717
Woody
Author by

Woody

Updated on July 09, 2022

Comments

  • Woody
    Woody almost 2 years

    Angular 5 authentication app using angularfire2 and firebase. The app works fine navigating using in-app links e.g. redirect to dashboard after login or link to another page (component) via a button/link in the app. However, if when on the "http://localhost:4300/dashboard" page I hit the browser refresh (Chrome), it redirects me back to the Login page. Using BACK / NEXT on the browser works fine - but I guess because I am not specifically asking to go to a particular route.

    I have a NavBar that, through use of subscription, identifies whether I am logged in or not (see screenshot top right...) - and this all works fine.

    Login page

    I am guessing that on browser refresh or direct URL navigation that it tries to load the page before identifying whether I am already authenticated or not. The dev console suggests this from the console.log statements I inserted into the nav-bar component and the fact they are "undefined" before Angular core suggests we are running in dev mode:

    Developer Tools Console

    app.routes:

    import { Routes, RouterModule } from '@angular/router';
    
    import { LoginComponent } from './views/login/login.component';
    import { DashboardComponent } from './views/dashboard/dashboard.component';
    import { ProfileComponent } from './views/profile/profile.component';
    
    import { AuthGuard } from './services/auth-guard.service';
    
    const appRoutes: Routes = [
      {
        path: '',
        component: LoginComponent
      },
      {
        path: 'dashboard',
        canActivate: [AuthGuard],
        component: DashboardComponent
      },
      {
        path: 'profile',
        canActivate: [AuthGuard],
        component: ProfileComponent
      },
      {
        path: '**',
        redirectTo: ''
      }
    ];
    
    export const AppRoutes = RouterModule.forRoot(appRoutes);
    

    auth-gaurd:

    import { AuthService } from './auth.service';
    import { Injectable } from '@angular/core';
    import { Router, CanActivate } from '@angular/router';
    
    @Injectable()
    export class AuthGuard implements CanActivate {
      status: string;
    
      constructor(private router: Router,
                  private authService: AuthService) { }
    
      canActivate() {
        this.authService.authState.subscribe(state =>
          this.status = state.toString());
    
        console.log('Can Activate ' + this.authService.authState);
        console.log('Can Activate ' + this.authService.isLoggedIn());
        console.log('Can Activate ' + this.status);
    
        if(this.authService.isLoggedIn()) {
          return true;
        }
    
        this.router.navigate(['/']);
        return false;
      }
    }
    

    auth.service:

    import { Injectable } from '@angular/core';
    import { Router } from "@angular/router";
    
    import { AngularFireAuth } from 'angularfire2/auth';
    import * as firebase from 'firebase/app';
    import { Observable } from 'rxjs/Observable';
    import { GoogleAuthProvider, GoogleAuthProvider_Instance } from '@firebase/auth-types';
    import { userInfo } from 'os';
    import { Subject } from 'rxjs/Subject';
    
    @Injectable()
    export class AuthService {
      private user: Observable<firebase.User>;
      private userDetails: firebase.User = null;
    
      public authState = new Subject();
    
      constructor(private _firebaseAuth: AngularFireAuth, private router: Router) { 
        this.user = _firebaseAuth.authState;
    
        this.user.subscribe((user) => {
          if (user) {
            this.userDetails = user;
            this.authState.next('Logged In');
            //console.log(this.userDetails);
          } else {
            this.userDetails = null;
            this.authState.next('Not Logged In');
          }
        });
      }
    
      isLoggedIn() {
        if (this.userDetails == null) {
          return false;
        } else {
          return true;
        }
      }
    }
    

    nav-bar.component:

    import { Component, OnInit } from '@angular/core';
    import { AuthService } from '../../services/auth.service';
    
    @Component({
      selector: 'app-nav-bar',
      templateUrl: './nav-bar.component.html',
      styleUrls: ['./nav-bar.component.css']
    })
    export class NavBarComponent implements OnInit {
      status: string;
    
      constructor(private authService: AuthService) {
        console.log('Constructor ' + this.status);
      }
    
      ngOnInit() {
        //this.authService.isLoggedIn().subscribe((state) => this.status = state.toString());
        this.authService.authState.subscribe(state =>
          this.status = state.toString());
        console.log('ngOnInit ' + this.status);
      }
    }
    
  • Woody
    Woody about 6 years
    Thanks. I have recently done something similar and it does work OK. However, I still get a flash of the login page until it determines I have logged in and the REDIRECT kicks in...
  • Nimatullah Razmjo
    Nimatullah Razmjo over 5 years
    @Woody, I have the same issue I get flash of login page until it determines. How did you solve it?
  • Woody
    Woody over 5 years
    @NimatullahRazmjo. Sorry but I stalled the project shortly afterwards - work got in the way (sigh)... There is still a flash so if you do succeed, would be great to hear the solution...
  • Nimatullah Razmjo
    Nimatullah Razmjo over 5 years
    @woody, I have used a library which is github.com/serhiisol/ngx-auth. I have implemented the PrectectedGuad to all my routes. it was working fine, but when I implemented Angular universal. The ngx-auth protected guard used to bring the login page for a second and then show the original routes.It used to happen in production. I have removed (Protected Guard) and implemented my own guard. now it works find.