Angular 5/6: protect route (route guard) without redirecting to error route

Ivan Hušnjak picture Ivan Hušnjak · May 23, 2018 · Viewed 7.1k times · Source

I have a bit of a pickle. I am using Route guard (implementing CanActivate interface) to check if user is granted access to particular route:

const routes: Routes = [
    {
        path: '',
        component: DashboardViewComponent
    },
    {
        path: 'login',
        component: LoginViewComponent
    },
    {
        path: 'protected/foo',
        component: FooViewComponent,
        data: {allowAccessTo: ['Administrator']},
        canActivate: [RouteGuard]
    },
    {
        path: '**',
        component: ErrorNotFoundViewComponent
    }
];

Now it works great in protecting the '/protected/foo' route from activating, but I would like to tell the user that route he is trying to access is forbidden (similar to 403 Forbidden you may get from server).

The problem: How do I show the user this special error view without redirecting him to error route which seams to be the preferred option by so many sources I have found? And how do I still use my RouteGuard without actually loading the forbidden route, because if I check access inside my FooViewComponent and display different view it kind of defeats point of having RouteGuard in the first place.

Ideally I would like to have my RouteGuard not only returning false in canActivate() method, but also replace component completely with say ErrorForbiddenViewComponent. But I have no idea how to do it, or is it event possible. Any alternatives?

This is how my route guard looks now:

import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {AuthService} from '../services/auth.service';

@Injectable()
export class RouteGuard implements CanActivate {

    constructor(
        private router: Router,
        private auth: AuthService
    ) {}

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const { auth, router } = this;
        const { allowAccessTo } = next.data;
        const identity = auth.getIdentity();
        if (
            identity &&
            allowAccessTo.indexOf(identity.role)
        ) {
            // all good, proceed with activating route
            return true;
        }
        if (identity) {
            // TODO show ErrorForbiddenViewComponent instead of redirecting
            console.log('403 Forbidden >>', next);
        }
        else { 
            // not logged in: redirect to login page with the return url
            const [returnUrl, returnQueryParams] = state.url.split('?');
            console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
            router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
        }
        return false;
    }
}

So I am just preventing route from loading, but I am not redirecting. I only redirect non logged visitors to login route.

Reasoning:

  • Routes should reflect certain state of application - visiting a route url should recreate that state
  • To have error routes (except for 404 Not Found) would mean your application can actually recreate error states. This makes no sense as why would you keep error state as state of your application? For debugging purpose one should use logs (console or server), revisiting error page (i.e. page refresh) might interfere with that.
  • Also by redirecting to error route app should provide some insights of error to user. For that matter either some parameter would need to be passed via url or (far worse) keeping the error sate in some error service and retrieve it upon accessing error route.
  • Also, ignoring the RouteGuard and just loading the component and checking access inside it may result in some extra dependencies loaded which would not be used anyway (as user is not allowed), makes the whole lazy loading much harder.

Does anyone have some kind of solution for this? I also wonder how come that after Angular 2+ being around for so long nobody had this kind of situation before? Everybody is just ok with redirecting?

Also keep in mind that although I am currently using the FooViewComponent synchronously, that may change in future!

Answer

planet_hunter picture planet_hunter · Jun 1, 2018

I had once worked on the similar problem.

Sharing my stackblitz poc where I have created -

  • Authenticated Component (with guard)
  • Login Component
  • Permission Guard
  • Route (/auth route is provided with PermissionGuardService guard)

The guard is evaluating the user type and handling the redirection / error accordingly.

The use cases are -

  • User is not logged in (shows a toast with log in message)
  • User is not admin (shows a toast with unauthorised message)
  • User is admin (show a toast with success messaage)

I have stored the user in local storage.

EDIT - DEMO enter image description here

Let me know if you need a special handling in it and I will update the code base.

Cheers!