I’ve been developing in Angular for many years now, and something that either passed me by, or I just never ran into a problem with it, is that if a route has multiple route guards, they will all run at once asynchronously. Obviously the result of all of these are waited on, and the route will not navigate unless the guards succeed, but the guards themselves all fire at once. In some scenarios this is desirable for speed, but in others… a little less so.
I had a scenario where I had two route guards. The first was simply to check if the user was logged in via an OAuth process. The second guard was to check if the user had a particular role, but it essentially depended on the first route guard checking the login status. I assumed (wrongly), that this set up would work as the first guard checks if the user is logged in, *then* we check the role they have in the second guard. Instead, I ran into multiple issues where the second guard would fire before the first, breaking my entire application.
The solution is that we need a “Combined” guard to run a given set of guards one after the other, synchronously. I made one like so :
@Injectable({
providedIn: 'root',
})
export class CombinedGuard implements CanActivate {
constructor(private injector: Injector) {
}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise {
const guards = route.data.guards || [];
for (const guard of guards) {
const instance: CanActivate = this.injector.get(guard);
let result = instance.canActivate(route, state);
//Depending on the route result, we may need to await upon it in different ways.
if(result instanceof Promise) {
result = await result;
}
if(result instanceof Observable) {
result = await result.toPromise();
}
if (result === false || result instanceof UrlTree) {
return result;
}
}
return true;
}
}
To use this, we need to use the data property of a route to set the guards rather than the guard property :
{
path: 'admin',
canActivate: [CombinedGuard],
data: {
guards : [LoginCheckGuard, AdminRoleGuard],
}
}
Notice how we pass our two guards (LoginCheckGuard and AdminRoleGuard) as data properties, not inside the canActivate, and instead pass the CombinedGuard as the actual canActivate guard.
Surprisingly, I found many people with the same issue complaining on the Angular GIT repository, but as of yet, no official fixes. It does seem slightly strange to me as running guards in a particular order does seem like a common use case, but for now, you’ll have to use the CombinedGuard.
💬 Leave a comment