Files
2022-02-09 23:49:57 -08:00

13 KiB
Raw Blame History

title, description, published, authors, tags, attached, license, originalLink
title description published authors tags attached license originalLink
Angular Route Guards For Authorization In A Web And Mobile Application Learn how to use Angular route guards for authenticating & authorizing access to certain child and parent routes. 2018-07-13T22:12:03.284Z
crutchcorn
angular
cc-by-4 https://www.thepolyglotdeveloper.com/2018/07/angular-route-guards-authorization-web-mobile-application/

Youre about to release your new Angular web app. Its a photo sharing site and you want to test it, so you send a link to it to your hacker sister. Shes always messing with your stuff and she found out the URL to your admin page you added to your web app. Before you know it, shes flushed your database using a button on that admin page that you didnt restrict access to. Not a problem when using development data - but Im sure your users wouldnt be any too keen on a service where they lost all of their data. Lets fix that

Component Checks

The most basic way to restrict a users access to any given page is to use logic that will run at the load time of the component and redirect the user if needed. Given Angulars lifecycle hooks, we can use ngOnInit in order to do so.

import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {MyAuthService} from '../../core/auth/auth.service';

@Component({
    selector: 'super-secret-component',
    templateUrl: '<comp-here></comp-here>',
    styles: []
})
export class SecretComponent implements OnInit {

    constructor(private myAuthService: MyAuthService, private router: Router) { }

    ngOnInit() {
        this.myAuthService.checkAuth().subscribe(isAllowed => {
            if (!isAllowed) {
                this.router.navigate(['/']);
            }
        })
    }

}

This code sample is pretty straightforward - on loading the component, lets go ahead and check that the user is allowed to see the page, if not - lets move them to somewhere they are allowed to see. You could even add a snackbar to let them know that theyre trying to access something they shouldnt, maybe move them to a login page? Its fairly customizable.

This works perfectly fine if theres a single route youd like to restrict users from being able to see, but perhaps youd like to lockdown an entire modules routes (or just a route with child routes) or there are many different routes youd like to protect in a similar way. Of course, you could always copy and paste the code weve made before, but Angular actually provides a much easier, cleaner way of doing so.

Introducing: Route Guards

In essence, a route guard is simply a check to tell if youre allowed to view a page or not. It can be added to any route using canActivate (a fairly verbose property, Id say) with a custom interface that follows Angulars CanActivate API. The most simplistic example of a router guard is as follows:

// route.guard.ts
import {Injectable} from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';

@Injectable()
export class RouteGuard implements CanActivate {

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        return true;
    }

}
// route-routes.module.ts
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {RouteComponent} from './posts.component';
import {RouteGuard} from '../core/route.guard';

const routes: Routes = [
    {
        path: '',
        pathMatch: 'full',
        component: RouteComponent,
        canActivate: [RouteGuard]
    }
];

export const routeRoutedComponents = [
    RouteComponent
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class RouteRoutingModule { }

As you can see from the typing of canActivate, Angular is fairly lenient with what you need to return in order to let a user to access the page or not - it accepts a Promise or Observable of a boolean or even just a boolean itself as a return value. This guard has limited value currently, because it always returns true regardless of any parameters or changes. However, if we replace the canActivate method with something a little more useful, we can easily add back the functionality our old ngOnInit had:

canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.myAuthService.checkAuth();
}

Tada! We suddenly have the same logic as before! We can now remove the ngOnInit from the previously added route, and keep things just as secure as before! Because we can return an Observable, we can even use Observable pipes like so:

return this.myAuthService.checkAuth().pipe(tap(allowed => {
    if (allowed) {
        this.snackBar.open("Welcome back!");
    }
}));

Of course, it might not be the best bet to add this logic in a guard, but its still representative of what youre capable of doing inside of a guard.

Children Guarding

When I first learned about this, I thought it was the coolest thing in the world. I started adding it to all of my routes. Next thing I knew, I was adding it to all my routes I wanted protected in some form or another.

[
    { path: '', pathMatch: 'full', component: RouteComponent, canActivate: [RouteGuard] },
    { path: 'list', component: RouteComponent, canActivate: [RouteGuard] },
    { path: 'detail/:id', component: RouteComponent, canActivate: [RouteGuard] }
];

This isnt too bad alone - but when you have hundreds of routes on a large scale project, this easily becomes unmanageable. I also had times when I wanted to add additional security to a routes children, for example a dashboard page that included some admin routes that I wanted to lock down. This is where child guards come into play.

Child guards do exactly what you think they would. They add an additional guard for children. They use a similar API as canActivate, and the reference to that API can be found here. So, if I were to add the following guard to my child routes:

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild} from '@angular/router';
import {Observable} from 'rxjs';

@Injectable()
export class ChildGuard implements CanActivateChild {
    canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        console.log('This child was activated!');
        return true;
    }
}

It would console log This child was activated every time you accessed a child route. How do you apply this to your routes?

[
    { path: '', canActivateChild: [ChildGuard], children: [
        { path: '', pathMatch: 'full', component: RouteComponent, canActivate: [RouteGuard] },
        { path: 'list', component: RouteComponent, canActivate: [RouteGuard] },
        { path: 'detail/:id', component: RouteComponent, canActivate: [RouteGuard] }      
    ]}
];

However, Im sure youre wondering what happens if you apply a canActivate alongside a canActivateChild. If you were to change the code so the canActivate runs a console.log('This is the canActivate!') and your routes were to look like this:

[
    { path: '', canActivate: [ActivateGuard], canActivateChild: [ChildGuard], children: [
        { path: '', pathMatch: 'full', component: RouteComponent },
        { path: 'list', component: RouteComponent },
        { path: 'detail/:id', component: RouteComponent }
    ]}
];

And accessed the list route, your console would output This is the canActivate! and then This child was activated!. Of course, this has limited application when making an empty route without a component to load (thats not a child), but its massively helpful when you have a component in the parent route such as this:

[
    {
        path: '',
        canActivate: [AuthenticationGuard],
        canActivateChild: [AuthorizationGuard],
        children: [
            { path: 'admin', component: AdminComponent },
        ]
    }
];

// NOT SHOWN: AuthenticationGuard and AuthorizationGuard. Just pretend they're code that checks what you think they would, based on the names (remember, authentication is if the user is who they say they are [AKA logged in]; authorization is making sure they have the right access [AKA if they're admin])

In this example, when you access the '' path, youll make sure the user is authenticated, but doesnt care about authorization. However, when you access a child of that path (in this example, 'admin'), it will check both authentication AND authorization.

Route Data

But lets say that I wanted to be able to change my child route based on information that Ive stored about that particular route. For example, I typically layout my breadcrumbs by using a data property on my routes like such:

[
    {
        path: '',
        canActivateChild: [ChildGuard],
        component: RouteComponent,
        data: {
            title: 'Main Page'
        },
        children: [
            { path: 'list', component: RouteComponent, data: {title: 'List Page'} },
            { path: 'detail/:id', component: RouteComponent, data: {title: 'Detail Page'} }
        ]
    }
];

If I wanted to be able to add a welcome message for each page that printed their title on every route you accessed, you could add that logic to a ChildGuard logic.

canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { // The return type has been simplified
    console.log(childRoute.data.title);
    return true;
}

Because the first argument to canActivateChild is an ActivatedRouteSnapshot, you can grab any of the methods or properties from the API from the routes that are currently being called. However, something youll probably want to keep in mind is that this will occur once for every single child route being called.

Lazy Loading

Because lazy loading using loadChildren is still considered a child route, all of the same rules from Children Guarding still apply. However, there are more tricks that are available for lazy loaded routes that are not otherwise available.

Can Load

The API for canLoad looks very similar to what weve seen before with canActivate and canActivateChild.

import {Injectable} from '@angular/core';
import {Route, CanLoad} from '@angular/router';
import {Observable} from 'rxjs';

@Injectable()
export class LoadGuard implements CanLoad {

    canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
        return true;
    }

}

Much like before, if we were to add it on a lazy loaded route:

[
    {
        canLoad: [LoadGuard],
        path: '',
        loadChildren: './feature.module#FeatureModule'
    }
]

It would prevent the route from loading if the return was false. However, the bigger difference between, say, canActivateChild, is how it interacts with the other methods shown here.

Lets say we have some routes shown like this:

[
    {
        path: '',
        canActivate: [AuthenticationGuard],
        canActivateChild: [AuthorizationGuard],
        children: [{
            canLoad: [LoadGuard],
            path: 'feature',
            loadChildren: './feature.module#FeatureModule'
        }, {
            path: 'otherfeature',
            component: RouteComponent
        }]
    }
];

In this example, if you access the '' route, only the AuthenticationGuard would be called. Meanwhile, if you accessed the 'otherfeature' route, you would load the AuthenticationGuard, THEN call the AuthorizationGuard. What order would you think the guards would load for the 'feature' route? The answer might be a little more tricky than you expect.

The answer? canLoad runs first. Before AuthenticationGuard and before AuthorizationGuard. It also, unlike the other two, prevents the entire loading of the route. The advantage here is that you can stop the loading of a lazy-loaded route before doing any checks youd want to run to prevent. This would increase performance greatly in situations where youd block the loading of a page and it would be much more secure. After canLoad runs, then the other two run in order as they would before

Wrap Up

Just like anything else, an Angular Router Guard is a tool. It has many uses that are really only restricted by how youre able to utilize that tool. Youre able to do service calls, logic changes, and more in order to restrict access to a page. However, its not a one-tool-fits-all solution. There will be times that a resolver might be able to help better, or sometimes even component logic might fit your use-case better. That being said, Guards are incredibly helpful when the time comes to use them