Auth guard prevents access even when user is already login

Viewed 876

When user first login with their credentials, the auth guard allows the user to access the admin pages but once I refresh or go to another admin page, the auth guard will redirect me back to the client side page, stating that I do not have access.

auth.guard.ts

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  CanActivate,
  Router,
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(public authService: AuthService, public router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    
    if (this.authService.isLoggedIn() !== true || this.authService.isAuthenticated() == null) {
      window.alert('Access not allowed!');
      this.router.navigate(['']);
    }
    return true;
  }
}

What my auth guard does is it checks if the token exists in the localstorage. If it exists, it then checks if the user exists within the database by retrieving the token, decoding it and adding it to a get method. The isAuthenticated function works on the first try but on subsequent tries, it does not even run at all and I am really confused on why it is not running.

auth.services.ts

import { Injectable } from '@angular/core';

import { Router } from '@angular/router';
import {
  HttpClient,
  HttpHeaders,
  HttpErrorResponse,
} from '@angular/common/http';

import { Observable, throwError} from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { User } from '../shared/IUser';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  API_URL: string = 'http://localhost:3000';
  headers = new HttpHeaders().set('Content-Type', 'application/json');
  currentUser = {};
  userId: string;

  
  constructor(private httpClient: HttpClient, public router: Router) {}

  login(user: User): Observable<any> {
    return this.httpClient.post<User>(`${this.API_URL}/login`, user);
  }

  getAccessToken() {
    return localStorage.getItem('access_token');
  }

  isLoggedIn(): boolean {
    let authToken = localStorage.getItem('access_token');
    return authToken !== null ? true : false;
  }

  isAuthenticated() {
    let authToken = this.getAccessToken();
    const claimString = atob(authToken.split('.')[1]);
    const claims = JSON.parse(claimString);


    this.getUserProfile(claims.userId).subscribe((res) => {
      this.userId = res._id;
    });
  
    
    return this.userId;
  }

  logout() {
    if (localStorage.removeItem('access_token') == null) {
      this.router.navigate(['auth']);
    }
  }

  getUserProfile(id): Observable<any> {
    return this.httpClient
      .get(`${this.API_URL}/users/${id}`, { headers: this.headers })
      .pipe(
        map((res: Response) => {
          return res || {};
        }),
        catchError(this.handleError)
      );
  }

  handleError(error: HttpErrorResponse) {
    let msg = '';
    if (error.error instanceof ErrorEvent) {
      // client-side error
      msg = error.error.message;
    } else {
      // server-side error
      msg = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(msg);
  }
}

1 Answers

The problem you are facing is related to the way observables work

  isAuthenticated() {
    let authToken = this.getAccessToken();
    const claimString = atob(authToken.split('.')[1]);
    const claims = JSON.parse(claimString);


    this.getUserProfile(claims.userId).subscribe((res) => {
      this.userId = res._id;
    });
  
    
    return this.userId;
  }

So the thing you are doing here is making a request that will save some value inside the this.userId. So the thing that happens is that when you try to access the guarded path, you still don't have the response from this.getUserProfile so the field this.userId is undefined.

A thing that you can do is to change your code to look like so

  getAccessToken() {
    return of(localStorage.getItem('access_token'));
  }
...
  isAuthenticated() {
    let authToken = this.getAccessToken();
    const claimString = atob(authToken.split('.')[1]);
    const claims = JSON.parse(claimString);


    return this.getUserProfile(claims.userId)
  }
...

And inside you guard to something like

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  CanActivate,
  Router,
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(public authService: AuthService, public router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return combineLatest([this.authService.isLoggedIn(), this.authService.isAuthenticated()]).pipe(map(([auth, id]) => auth === true && id !== null))

  }
}

This is sample code so there might be some typos, but I think you will get the idea :)

Related