angulartypescriptauthorizationangular-guards

How to wait for server authorization in an Angular role guard?


In my application, the user logs in and receives a JWT, which is stored in local storage. After the user is authenticated, there's a following call to the server to determine the user's roles and functions (what pages they can visit).

My problem is that when a user wants to open a page (resuming old session, duplicating tab, passing URL to someone else, etc.), the app doesn't have authorization details and must first request them, the role guard kicks in. This results in the user being redirected to the login page.

@Injectable({
  providedIn: 'root'
})
export class RoleGuardService implements CanActivate {

  constructor(public auth: AuthService, public router: Router, public globalConfig: GlobalConfigService) { }

  canActivate(route: ActivatedRouteSnapshot): boolean {

    if (!this.auth.isAuthenticated()) {
      this.router.navigate(['login']);
      return false;
    }

    const expectedFunction = route.data.expectedFunction;

    if (!this.globalConfig.hasFunction(expectedFunction)) {
      this.router.navigate(['login']);
      return false;
    }

    return true;
  }
}

The expected function is defined in routes, example:

{
    path: 'system-admin', loadChildren: () => SystemAdminModule,
    data: { breadcrumb: 'System Admin', expectedFunction: FunctionType.SystemAdministration }, canActivate: [RoleGuard]
},

The hasFunction body in the GlobalConfigService looks like this:

private authorizedUser: AuthorizedUser = new AuthorizedUser();

public hasFunction(expectedFunction: FunctionType): boolean {
    return !!this.authorizedUser.functions
            && this.authorizedUser.functions.find(f => f === expectedFunction) !== undefined;
}

Authorization done in the AuthService is done as follows:

public onAuthorized = new Subject<AuthorizedUser>();

authorize() {
    const url = environment.APIURL + 'auth/currentuser';

    return this.http.get(url).subscribe(
        resp => {
            this.globalConfig.AuthorizedUser = resp;
            this.onAuthorized.next(resp as AuthorizedUser);
        }
    );
}

And authorize() is called from the ngOnInit() in the AppComponent

ngOnInit(): void {
    if (this.auth.isAuthenticated()) {
      this.auth.authorize();
    } else {
      this.router.navigate(['login']);
    }
}

I believe the solution would be to put in some wait condition if the user is authenticated, then authorization should be allowed to complete before anything else is being evaluated. Would this need to happen in RoleGuard only, or would it span throughout the whole authentication/authorization process?


Solution

  • Yes, you can wait for user to authorize inside your guard. The only one thing you need to bear in mind is not to authorize user twice, meaning you should cache authorization result between page navigation.

    role-guard.service.ts

    canActivate(route: ActivatedRouteSnapshot): boolean | Promise<boolean> {
    
      if (!this.auth.isAuthenticated()) {
        this.router.navigate(['login']);
        return false;
      }
    
      return this.auth.authorize().then(result => {
        if (!result) {
          return false;
        }
    
        const expectedFunction = route.data.expectedFunction;
    
        if (!this.globalConfig.hasFunction(expectedFunction)) {
          this.router.navigate(['login']);
          return false;
        }
    
        return true;
      });
    }
    

    auth.service.ts

    @Injectable({
      providedIn: 'root',
    })
    class AuthService {
      ...
    
      private authorizePromise: Promise<boolean>;
    
      constructor(private http: HttpClient, private globalConfig: GlobalConfigService) {}
    
      authorize(): Promise<boolean> {
        if (!this.authorizePromise) {
          const url = environment.APIURL + 'auth/currentuser';
          this.authorizePromise = this.http.get(url)
            .toPromise()
            .then(resp => {
              this.globalConfig.AuthorizedUser = resp;
              this.onAuthorized.next(resp as AuthorizedUser);
              return true;
            })
            .catch(() => false);
        }
    
        return this.authorizePromise;
      }
    }
    

    As you can see I used cached authorizePromise in AuthService in order to cache authorization result so that authorization will happen only once.

    Here are also some snippets in live example