Angular 5 canActivate redirecting to login on browser refresh
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
Woody
Updated on July 09, 2022Comments
-
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.
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:
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 about 6 yearsThanks. 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 over 5 years@Woody, I have the same issue I get flash of login page until it determines. How did you solve it?
-
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 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 implementedAngular universal
. Thengx-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.