devZ
devZ

Reputation: 726

Angular's AuthGuard allways return false on page refresh, but I am authenticated

I am creating login system in my Angular application. Everything works fine, I have just one problem with my AuthGuard. When I refresh the page in which implement 'canActivate: [AuthGuard]' it redirects me to login and i get isAuth = false. But from there I can go back to my guarded page without login.

AuthGuard

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private userAuthService: UserAuthService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | boolean
    | UrlTree
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree> {
    const isAuth = this.userAuthService.getIsAuth();
    console.log(isAuth + ' at auth.guard.ts');

    if (!isAuth) {
      this.router.navigate(['/login']);
    }
    return isAuth;
  }
}

And my UserAuthService file:

  private authStatusListener = new Subject<boolean>();
  isAuthenticated: boolean = false;
  private token: string | null = '';

 getIsAuth() {
    return this.isAuthenticated;
  }

  getAuthStatusListener() {
    return this.authStatusListener.asObservable();
  }

  login(user: UserLogin) {
    const url = environment.apiUrl + '/auth/login/';

    this.http.post<{ token: string }>(url, user).subscribe(
      (res: any) => {
        const token = res.token;
        this.token = token;
        if (this.token) {
          this.isAuthenticated = true;
          this.authStatusListener.next(true);
          this.storeJwtToken(this.token);
          this.router.navigate(['/']);
        } else {
          this.isAuthenticated = false;
          this.authStatusListener.next(false);
        }
      },
      (error) => {
        this.authStatusListener.next(false);
      }
    );
  }

  autoAuthUser() {
    const token = this.getJwtToken();
    if (token) {
      this.token = token;
      this.isAuthenticated = true;
      this.authStatusListener.next(true);
    } else {
      return;
    }
  }

  storeJwtToken(token: string) {
    localStorage.setItem('token', token);
  }

  getJwtToken(): any {
    const token: any = localStorage.getItem('token');
    return token;
  }

And in my app.component.ts file I am just doing this:

  ngOnInit(): void {
    this.userAuthService.autoAuthUser();
  }

Upvotes: 1

Views: 1972

Answers (2)

Cristian-Florin Calina
Cristian-Florin Calina

Reputation: 1008

You can return an Observable in canActivate (docs)

You can just use the getAuthStatusListener in the guard instead of that boolean.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private userAuthService: UserAuthService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | boolean
    | UrlTree
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree> {

    return this.userAuthService.getAuthStatusListener().pipe(
      take(1),
      map(isAuthenticated => {
        if (!isAuthenticated) {
          this.router.navigate(['/login']);
        }

        return isAuthenticated;
      })
    )
  }
}

Also, in your service, you need to emit false on the else of autoAuthUser

autoAuthUser() {
  const token = this.getJwtToken();
    
  this.token = token;
  this.isAuthenticated = !!token;
  this.authStatusListener.next(!!token);
}

Upvotes: 1

Ankush
Ankush

Reputation: 304

You can create a promise (async) function in your UserAuthService and provide that function in app.module.ts as dependency. So, it will wait until resolves the promise of your async function before starting the app. With this you will be already logged in when your app starts or refresh. UserAuthService:

  async init(): Promise<any>{
    this.autoAuthUser();
  }

app.module.ts:

@NgModule({
  declarations: [...],
  imports: [...],
  providers: [
    UserAuthService,
    {
      //With this your app will wait to resolve the promise of init() of your UserAuthService.
      provide: APP_INITIALIZER, 
      useFactory: (service: UserAuthService) => function() { return service.init();},
      deps: [UserAuthService],
      multi: true
    }],
  bootstrap: [AppComponent]
})
export class AppModule { }

Another way

You can simply redirect from your autoAuthUser() function.

AuthGuard:

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private userAuthService: UserAuthService,
    private router: Router,
    state: RouterStateSnapshot
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ){
    const isAuth = this.userAuthService.getIsAuth();
    console.log(isAuth + ' at auth.guard.ts');
    if (!isAuth) {
      //Here you pass queryParams with last url from state.url.
      this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    }
    return isAuth;
  }
}

UserAuthService :

constructor(
    private route: ActivatedRoute,
    private router: Router){}

autoAuthUser() {
    const token = this.getJwtToken();
    if (token) {
      this.token = token;
      this.isAuthenticated = true;
      this.authStatusListener.next(true);
      // Here you redirect to the last URL or '/'.
      this.router.navigateByUrl(this.route.snapshot.queryParams['returnUrl'] || '/');
    } else {
      return;
    }
  }

Upvotes: 2

Related Questions