1

I have an observable to check if the user is loggedIn or not. I set that observable from auth api which is called from appComponent when application starts.

export class AppComponent implements OnInit {
    
      loggedIn$: Observable<boolean | null>;
    
      constructor(private userService:UserService) {
        this.userService.authenticate();
      }
    
      ngOnInit(): void {
        //to show header when loggedin
        this.loggedIn$ = this.userService.isAuthenticated;
      }
    }

and I have AuthGaurd which restricts unauthenticated users from navigating to inner pages.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
        return this.userService.isAuthenticated.pipe(
            take(1),
            map((loggedin:boolean) => {
                if(loggedin) {
                    //checking for required permissions as well
                    let permission = route.data['permission'];
                    if( this.userService.hasPermission(permission)) {
                        return true; 
                    }
                }
                this.router.navigate(['unauthorised-user', { page: state.url}]);
                return false;

            })
        );
    }

and here's aut service

    private loggedIn: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
    public authenticate() {
        this.http.get<User>('<auth-api-url')
          .subscribe(
            (response: user) => {
              this.user = response;
              if (this.user) {
                document.cookie = `coockie`;
                this.loggedIn.next(true);
                this.getReferenceData();
              }else {
                this.loggedIn.next(false);
              }
            }
          );
      }
     
     get isAuthenticated(): Observable<boolean> {
         return this.loggedIn.asObservable();
     }
        
    public hasPermission(permission:string): boolean {
        return this.user?.permissions?.includes(permission);
      }

when I launch the app "/" appComponent makes call to auth api but auth guard checks and redirects user to unauthorised-user page as auth api hasn't finished yet.

I have looked at few solutions which adds auth api call into canActivate guard but then it calls api every time I navigate to different page. I would like to call the api once and then set the flag (observable) in auth service which other components can use to check if user is authenticated or not.

Thanks

Bhavesh
  • 819
  • 1
  • 17
  • 31
  • Does this answer your question? [Angular's AuthGuard allways return false on page refresh, but I am authenticated](https://stackoverflow.com/questions/72927656/angulars-authguard-allways-return-false-on-page-refresh-but-i-am-authenticated) – Ankush Jul 13 '22 at 15:17
  • His issue is slightly different. from @Muhammad's suggestion issues now are 1. how do I show spinner while waiting for auth api to complete and 2. if auth fails how do I redirect user to unauthorised-page? – Bhavesh Jul 14 '22 at 03:18

2 Answers2

1

The current implementation will always deceive you as app.component.ts is not the right entry point to place your authentication code. app.module.ts is the root point to place the authentication logic.

providers: [
{
  provide: APP_INITIALIZER,
  useFactory: (us: UserService) =>
    function () {
      return new Promise((resolve, reject) => {
        us.contextPopulate().subscribe(
          (user: any) => {
            resolve(true);
          },
          (error) => resolve(error)
        );
      });
    },
  deps: [UserService],
  multi: true,
},

],

  private currentUserSubject = new BehaviorSubject<any>(null);
  public currentUser = this.currentUserSubject.asObservable().pipe(distinctUntilChanged());

  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  public isAuthenticated = this.isAuthenticatedSubject.asObservable();

  contextPopulate() {
    // If JWT detected, attempt to get & store user's info
    if (this.jwtService.getToken()) {
      return this.apiService.get('auth/verify_auth_token').pipe(
        map((res) => {
          this.setCurrentUser(res);
          return res;
        }),
        catchError((e) => {
          this.purgeAuth();
          return of(null);
        })
      );
    } else {
      return of(null);
    }
  }
  setCurrentUser(user) {
    // Set current user data into observable
    this.currentUserSubject.next(user);
    // Set isAuthenticated to true
    this.isAuthenticatedSubject.next(true);
  }

Here, contextPopulate is the method that calls the auth endpoint each time your app refreshes/restarts/starts. It will not proceed until receive a response from the promise(either authorized or unauthorized).

Remove the code from app.component.ts. app.module.ts and auth method from userService is all you need to implement authentication.

Muhammad Umar
  • 1,291
  • 7
  • 13
  • can we use observable instead of Promise? – Bhavesh Jul 13 '22 at 05:31
  • we can, but it will not fix the problem. The use of Promise makes the application wait until the http call ends and bring a success or error response. – Muhammad Umar Jul 13 '22 at 05:34
  • Updated to: useFactory: (userService:UserService) => () => { return new Promise((resolve, reject) => { userService.authenticate().subscribe((_user:UserProfileDto | null) => { resolve(true); }, (error) => resolve(error)) }); }, – Bhavesh Jul 13 '22 at 06:00
  • this solution works but the problem is while its waiting for api response , user is looking at the blank page. Is there a way to show spinner while waiting and if auth fails redirect user to unauthorised page ? – Bhavesh Jul 13 '22 at 06:02
  • if you have an interceptor, I would suggest this https://www.npmjs.com/package/ng-http-loader . it will show a spinner every time an http call is initiated. – Muhammad Umar Jul 13 '22 at 06:21
  • I do use interceptor but don't want to use that as it will block the page for every single call (which feels like going back to old days before ajax) – Bhavesh Jul 14 '22 at 03:09
0
  1. Need some global service added in app.module.ts for store user info after login
  2. in service add:

loginned$ = new BehaviorSubject<boolean>(false);

  1. after calligc func login() on success add this.loginned$.next(true);

  2. Add Guard like below:

    @Injectable({providedIn: 'root'})
    export class NotAuthGuard implements CanActivate {
      constructor(private service: SomeService) {}
    
      async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        return of(this.service.loginned$.getValue()).toPromise();
      }
    }
    
  • yes with login page it quite simple, stay on login page till api is completed but here half of the problem is that there is no login page. It uses windows authentication (user of the PC/browser). – Bhavesh Jul 13 '22 at 05:18