Книга: Angular 2 Cookbook
Назад: Working with matrix URL parameters and routing arrays
Дальше: 7. Services, Dependency Injection, and NgModule

Adding route authentication controls with route guards

The nature of single-page applications wholly controlling the process of routing affords them the ability to control each stage of the process. For you, this means that you can intercept route changes as they happen and make decisions about where the user should go.

Note

The code, links, and a live example of this are available at .

Getting ready

In this recipe, you'll build a simple pseudo-authenticated application from scratch.

You goal is to protect users from certain views when they are not authenticated, and at the same time, implement a sensible login/logout flow.

How to do it...

Begin by defining two initial views with routes in your application. One will be a Default view, which will be visible to everybody, and one will be a Profile view, which will be only visible to authenticated users:

[app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RouterModule, Routes} from '@angular/router';   import {RootComponent} from './root.component';   import {DefaultComponent} from './default.component';   import {ProfileComponent} from './profile.component';      const appRoutes:Routes = [     {path: 'profile', component: ProfileComponent},     {path: '**', component: DefaultComponent}   ];      @NgModule({     imports: [       BrowserModule,       RouterModule.forRoot(appRoutes)     ],     declarations: [       DefaultComponent,       ProfileComponent,       RootComponent     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}     [app/default.component.ts]      import {Component} from '@angular/core';      @Component({     template: `       <h2>Default view!</h2>     `   })   export class DefaultComponent {}   [app/profile.component.ts]      import {Component} from '@angular/core';      @Component({     template: `       <h2>Profile view</h2>       Username: <input>       <button>Update</button>     `   })   export class ProfileComponent {}   

Obviously, this does not do anything yet.

Implementing the Auth service

As done in the Observables chapter, you will implement a service that will maintain the state entirely within a BehaviorSubject.

Note

Recall that a BehaviorSubject will rebroadcast its last emitted value whenever an Observer is subscribed to it. This means it requires setting the initial state, but for an authentication service this is easy; it can just start in the unauthenticated state.

For the purpose of this recipe, let's assume that a username of null means the user is not authenticated and any other string value means they are authenticated:

[app/auth.service.ts]      import {Injectable} from '@angular/core';   import {BehaviorSubject} from 'rxjs/BehaviorSubject';   import {Observable} from 'rxjs/Observable';      @Injectable()   export class AuthService {     private authSubject_:BehaviorSubject<any> =        new BehaviorSubject(null);     usernameEmitter:Observable<string>;        constructor() {       this.usernameEmitter = this.authSubject_.asObservable();       this.logout();     }        login(username:string):void {       this.setAuthState_(username);     }        logout():void {       this.setAuthState_(null);     }        private setAuthState_(username:string):void {       this.authSubject_.next(username);     }   }   

Note that nowhere are we storing the username as a string. The state of the authentication lives entirely within BehaviorSubject.

Wiring up the profile view

Next, make this service available to the entire application and wire up the profile view:

[app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RouterModule, Routes} from '@angular/router';   import {RootComponent} from './root.component';   import {DefaultComponent} from './default.component';   import {ProfileComponent} from './profile.component';   import {AuthService} from './auth.service';      const appRoutes:Routes = [     {path: 'profile', component: ProfileComponent},     {path: '**', component: DefaultComponent}   ];      @NgModule({     imports: [       BrowserModule,       RouterModule.forRoot(appRoutes)     ],     declarations: [       DefaultComponent,       ProfileComponent,       RootComponent     ],     providers: [       AuthService     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}   [app/profile.component.ts]      import {Component} from '@angular/core';   import {AuthService} from './auth.service';   import {Observable} from 'rxjs/Observable';      @Component({     template: `       <h2>Profile view</h2>       Username: <input #un value="{{username | async}}">       <button (click)=update(un.value)>Update</button>     `   })   export class ProfileComponent {     username:Observable<string>;        constructor(private authService_:AuthService) {        this.username = authService_.usernameEmitter;     }        update(username:string):void {       this.authService_.login(username);     }   }   

Tip

It's very handy to use the async pipe when interpolating values. Recall that when you invoke subscribe() on a service Observable from inside an instantiated view component, you must invoke unsubscribe() on the Subscription when the component is destroyed; otherwise, your application will have a leaked listener. Making the Observable available to the view saves you this trouble!

With the profile view wired up, add links and interpolate the username into the root app view in a navbar, to give yourself the ability to navigate around. You don't have to revisit the file; just add all the links you'll need in this recipe now:

[app/root.component.ts]      import {Component} from '@angular/core';   import {Router} from '@angular/router';   import {AuthService} from './auth.service';   import {Observable} from 'rxjs/Observable';      @Component({     selector: 'root',     template: `       <h3 *ngIf="!!(username | async)">         Hello, {{username | async}}.       </h3>       <a [routerLink]="['']">Default</a>       <a [routerLink]="['profile']">Profile</a>       <a *ngIf="!!(username | async)"          [routerLink]="['login']">Login</a>       <a *ngIf="!!(username | async)"          [routerLink]="['logout']">Logout</a>       <router-outlet></router-outlet>     `   })   export class RootComponent {     username:Observable<string>;        constructor(private authService_:AuthService) {       this.username = authService_.usernameEmitter;     }   }    

Tip

For consistency, here you are using the async pipe to make the component definition simpler. However, since you have four instances in the template referencing the same Observable, it might be better down the road to instead set one subscriber to Observable, bind it to a string member in RootComponent, and interpolate this instead. Angular's data binding makes this easy for you, but you would still need to deregister the subscriber when this is destroyed. However, since it is the application's root component, you shouldn't really expect this to happen.

Restricting route access with route guards

So far so good, but you will notice that the profile view is allowing the user to effectively log in willy-nilly. You would instead like to restrict access to this view and only allow the user to visit it when they are already authenticated.

Angular gives you the ability to execute code, inspect the route, and redirect it as necessary before the navigation occurs using a Route Guard.

Note

Guard is a bit of a misleading term here. You should think of this feature as a route shim that lets you add logic that executes before Angular actually goes to the new route. It can indeed "Guard" a route from an unauthenticated user, but it can also just as easily conditionally redirect, save the current URL, or perform other tasks.

Since the Route Guard needs to have the @Injectable decorator, it makes good sense to treat it as a service type.

Start off with the skeleton AuthGuardService defined inside a new file for route guards:

[app/route-guards.service.ts]      import {Injectable} from '@angular/core';   import {CanActivate} from '@angular/router';         @Injectable()   export class AuthGuardService implements CanActivate {     constructor() {}        canActivate() {       // This method is invoked during route changes if this       // class is listed in the Routes     }   }   

Before having this do anything, import the module and add it to Routes:

[app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RouterModule, Routes} from '@angular/router';   import {RootComponent} from './root.component';   import {DefaultComponent} from './default.component';   import {ProfileComponent} from './profile.component';   import {AuthService} from './auth.service';   import {AuthGuardService} from './route-guards.service';      const appRoutes:Routes = [     {        path: 'profile',        component: ProfileComponent,        canActivate: [AuthGuardService]      },     {        path: '**',        component: DefaultComponent      }   ];      @NgModule({     imports: [       BrowserModule,       RouterModule.forRoot(appRoutes)     ],     declarations: [       DefaultComponent,       ProfileComponent,       RootComponent     ],     providers: [       AuthService,       AuthGuardService     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}   

Now, each time the application matches a route to a profile and tries to navigate there, the canActivate method defined inside AuthGuardService will be called. The return value of true means the navigation can occur; the return value of false means the navigation is cancelled.

Tip

canActivate can either return a boolean or an Observable<boolean>. Be aware, should you return Observable, the application will dutifully wait for the Observable to emit a value and complete it before navigating.

Since the application's authentication state lives inside BehaviorSubject, all this method needs to do is subscribe, check the username, and navigate if it is not null. It suits this to return Observable<boolean>:

[app/route-guards.service.ts]      import {Injectable} from '@angular/core';   import {CanActivate, Router} from '@angular/router';   import {AuthService} from './auth.service';    import {Observable} from 'rxjs/Observable';      @Injectable()   export class AuthGuardService implements CanActivate {     constructor(private authService_:AuthService,        private router_:Router) {}        canActivate():Observable<boolean> {       return this.authService_.usernameEmitter.map(username => {          if (!username) {           this.router_.navigate(['login']);         } else {           return true;         }       });     }   }   

Once you implement this, you will notice that the navigation will never occur, even though the service is emitting the username correctly. This is because the recipient of the return value of canActivate isn't just waiting for an Observable emission; it is waiting for the Observable to complete. Since you just want to peek at the username value inside BehaviorSubject, you can just return a new Observable that returns one value and then is completed using take():

[app/route-guards.service.ts]   import {Injectable} from '@angular/core';   import {CanActivate, Router} from '@angular/router';   import {AuthService} from './auth.service';    import {Observable} from 'rxjs/Observable';   import 'rxjs/add/operator/take';      @Injectable()   export class AuthGuardService implements CanActivate {     constructor(private authService_:AuthService,        private router_:Router) {}        canActivate():Observable<Boolean> {       return this.authService_.usernameEmitter.map(username => {          if (!username) {           this.router_.navigate(['login']);         } else {           return true;         }       }).take(1);     }   }   

Superb! However, this application still lacks a method to formally log in and log out.

Adding login behavior

Since the login page will need its own view, it should get its own route and component. Once the user logs in, there is no need to keep them on the login page, so you want to redirect them to the default view once they are done.

First, create the login component and its corresponding view:

[app/login.component.ts]      import {Component} from '@angular/core';   import {Router} from '@angular/router';   import {AuthService} from './auth.service';      @Component({     template: `       <h2>Login view</h2>       <input #un>       <button (click)="login(un.value)">Login</button>     `   })   export class LoginComponent {     constructor(private authService_:AuthService,        private router_:Router) { }        login(newUsername:string):void {       this.authService_.login(newUsername);       this.authService_.usernameEmitter         .subscribe(username => {           if (!!username) {             this.router_.navigate(['']);           }         });     }   }    [app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RouterModule, Routes} from '@angular/router';   import {RootComponent} from './root.component';   import {DefaultComponent} from './default.component';   import {ProfileComponent} from './profile.component';   import {LoginComponent} from './login.component';   import {AuthService} from './auth.service';   import {AuthGuardService} from './route-guards.service';      const appRoutes:Routes = [     {        path: 'login',        component: LoginComponent     },     {        path: 'profile',        component: ProfileComponent,        canActivate: [AuthGuardService]      },     {        path: '**',        component: DefaultComponent      }   ];      @NgModule({     imports: [       BrowserModule,       RouterModule.forRoot(appRoutes)     ],     declarations: [       LoginComponent,       DefaultComponent,       ProfileComponent,       RootComponent     ],     providers: [       AuthService,       AuthGuardService     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}

You should now be able to log in. This is all well and good, but you will notice that with this implemented, updating the username in the profile view will navigate to the default view, exhibiting the same behavior defined in the login component. This is because the subscriber is still listening to AuthService Observable. You need to add in an OnDestroy method to correctly tear down the login view:

[app/login.component.ts]      import {Component, ngOnDestroy} from '@angular/core';   import {Router} from '@angular/router';   import {AuthService} from './auth.service';   import {Subscription} from 'rxjs/Subscription';      @Component({     template: `       <h2>Login view</h2>       <input #un>       <button (click)="login(un.value)">Login</button>     `   })   export class LoginComponent implements OnDestroy {     private usernameSubscription_:Subscription;          constructor(private authService_:AuthService,        private router_:Router) { }        login(newUsername:string):void {       this.authService_.login(newUsername);       this.usernameSubscription_ = this.authService_         .usernameEmitter         .subscribe(username => {           if (!!username) {             this.router_.navigate(['']);           }         });     }          ngOnDestroy() {       // Only invoke unsubscribe() if this exists       this.usernameSubscription_ &&         this.usernameSubscription_.unsubscribe();     }   }    

Adding the logout behavior

Finally, you want to add a way for users to log out. This can be accomplished in a number of ways, but a good implementation will be able to delegate the logout behavior to its associated methods without introducing too much boilerplate code.

Ideally, you would like for the application to just be able to navigate to the logout route and let Angular handle the rest. This, too, can be accomplished with canActivate. First, define a new Route Guard:

[app/route-guards.service.ts]      import {Injectable} from '@angular/core';   import {CanActivate, Router} from '@angular/router';   import {AuthService} from './auth.service';   import {Observable} from 'rxjs/Observable';   import 'rxjs/add/operator/take';         @Injectable()   export class AuthGuardService implements CanActivate {     constructor(private authService_:AuthService,        private router_:Router) {}        canActivate():Observable<boolean> {       return this.authService_.usernameEmitter.map(username => {          if (!username) {           this.router_.navigate(['login']);         } else {           return true;         }       }).take(1);     }   }      @Injectable()   export class LogoutGuardService implements CanActivate {       constructor(private authService_:AuthService,        private router_:Router) {}        canActivate():boolean {       this.authService_.logout();       this.router_.navigate(['']);       return true;     }   } 

This behavior should be pretty self-explanatory.

Tip

Your canActivate method must match the signature defined in the CanActivate interface, so even though it will always navigate to a new view, you should add a return value to please the compiler and to handle any cases where the preceding code should fall through.

Next, add the logout component and the route. The logout component will never be rendered, but the route definition requires that it is mapped to a valid component. So LogoutComponent will consist of a dummy class:

[app/logout.component.ts}      import {Component} from '@angular/core';      @Component({     template: ''   })   export class LogoutComponent{}   [app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RouterModule, Routes} from '@angular/router';   import {RootComponent} from './root.component';   import {DefaultComponent} from './default.component';   import {ProfileComponent} from './profile.component';   import {LoginComponent} from './login.component';   import {LogoutComponent} from './logout.component';   import {AuthService} from './auth.service';   import {AuthGuardService, LogoutGuardService}      from './route-guards.service';      const appRoutes:Routes = [     {        path: 'login',        component: LoginComponent     },     {        path: 'logout',        component: LogoutComponent,       canActivate: [LogoutGuardService]     },     {        path: 'profile',        component: ProfileComponent,        canActivate: [AuthGuardService]      },     {        path: '**',        component: DefaultComponent      }   ];      @NgModule({     imports: [       BrowserModule,       RouterModule.forRoot(appRoutes)     ],     declarations: [       LoginComponent,       LogoutComponent,       DefaultComponent,       ProfileComponent,       RootComponent     ],     providers: [       AuthService,       AuthGuardService,       LogoutGuardService     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}   

With this, you should have a fully functional login/logout behavior process.

How it works...

The core of this implementation is built around Observables and Route Guards. Observables allow your AuthService module to maintain the state and expose it simultaneously through BehaviorSubject, and Route Guards allow you to conditionally navigate and redirect at your application's discretion.

There's more...

Application security is a broad and involved subject. The recipe shown here involves how to smoothly move your user around the application, but it is by no means a rigorous security model.

The actual authentication

You should always assume the client can manipulate its own execution environment. In this example, even if you protect the login/logout methods on AuthService as well as you can, it will be easy for the user to gain access to these methods and authenticate themselves.

User interfaces, which Angular applications squarely fall into, are not meant to be secure. Security responsibilities fall on the server side of the client/server model since the user does not control that execution environment. In an actual application, the login() method here would make a network request get some sort of a token from the server. Two very popular implementations, JSON Web Tokens and Cookie auth, do this in different ways, but they are essentially variations of the same theme. Angular or the browser will store and send these tokens, but ultimately the server should act as the gatekeeper of secure information.

Secure data and views

Any secure information you might send to the client should be behind server-based authentication. For many developers, this is an obvious fact, especially when dealing with an API. However, Angular also requests templates and static files from the server, and some of these you might not want to serve to the wrong people. In this case, you will need to configure your server to authenticate requests for these static files before you serve them to the client.

See also

  • Navigating with the Router service uses an Angular service to navigate around an application
  • Building stateful RouterLink behavior with RouterLinkActive shows how to integrate application behavior with a URL state
  • Implementing nested views with route parameters and child routes gives an example of how to configure Angular URLs to support nesting and data passing
  • Working with matrix URL parameters and routing arrays demonstrates Angular's built-in matrix URL support
Назад: Working with matrix URL parameters and routing arrays
Дальше: 7. Services, Dependency Injection, and NgModule

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already