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.
The code, links, and a live example of this are available at .
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.
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.
As done in the Observables
chapter, you will implement a service that will maintain the state entirely within a BehaviorSubject
.
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
.
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); } }
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; } }
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.
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.
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.
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.
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(); } }
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.
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.
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.
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.
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.
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.