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?
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