May 17 2019

Angular 7 Tutorial Part 4 - Login Form, Authentication Service & Route Guard

Other parts available in Angular 7 tutorial series:


Angular 7 Tutorial Part 4

In part 4 of this Angular 7 tutorial series we're going to implement authentication with a login form, authentication service and an Angular route guard. We'll also setup a fake backend so we can test the example application without an API.

The complete source code for this part of the tutorial is available on github at https://github.com/cornflourblue/angular-7-tutorial in the part-4 folder. If you haven't completed Part 3 (Add Routing & Multiple Pages) but want to follow the steps in this part of the tutorial series you can start with the code in the part-3 folder of the github repo.

Steps:

  1. Create Fake Backend
  2. Create Global Config Object
  3. Create Authentication Service
  4. Import ReactiveFormsModule and HttpClientModule into App Module
  5. Add Form Logic to Login Component
  6. Add Form HTML to Login Component Template
  7. Add Logout and Show/Hide Nav to App Component
  8. Create Route Guard
  9. Create JWT Interceptor
  10. Create Error Interceptor
  11. Add HTTP Interceptors to App Module
  12. Start Angular 7 Application!


Create Fake Backend

Pretty much all Angular applications require a backend / server side API to function correctly and this app is no different, the login functionality we're building will work by sending user credentials (username and password) to an API via HTTP for authentication.

In order to run and test the Angular application before the API is built, we'll be creating a fake backend that will intercept the HTTP requests from the Angular app and send back "fake" responses. This is done by creating a class that implements the Angular HttpInterceptor interface, for more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor or this article.

The fake backend contains a handleRoute function that checks if the request matches one of the faked routes in the switch statement, at the moment this only includes POST requests to the /users/authenticate route for handling authentication. Requests to the authenticate route are handled by the authenticate function which checks the username and password against an array of hardcoded users. If the username and password are correct then an ok response is returned with the user details and a fake jwt token, otherwise an error response is returned. If the request doesn't match any of the faked routes it is passed through as a real HTTP request to the backend API.

Create Helpers Folder

Create a folder named _helpers in the /src/app folder.

The _helpers folder will contain all the bits and pieces that don't really fit into other folders but don't justify having a folder of their own. The underscore "_" prefix is used to easily differentiate between shared code (e.g. _services, _components, _helpers etc) and feature specific code (e.g. home, login, register), the prefix also groups shared component folders together at the top of the folder structure in VS Code.

Create Fake Backend Http Interceptor

Create a file named fake-backend.ts in the _helpers folder and add the following TypeScript code to it:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';

let users = [{ id: 1, firstName: 'Jason', lastName: 'Watmore', username: 'test', password: 'test' }];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        // wrap in delayed observable to simulate server api call
        return of(null)
            .pipe(mergeMap(handleRoute))
            .pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
            .pipe(delay(500))
            .pipe(dematerialize());

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }    
        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            if (!user) return error('Username or password is incorrect');
            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                token: 'fake-jwt-token'
            })
        }

        // helper functions

        function ok(body?) {
            return of(new HttpResponse({ status: 200, body }))
        }

        function error(message) {
            return throwError({ error: { message } });
        }
    }
}

export const fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};


Create Barrel File in Helpers Folder

Create a file named index.ts inside the _helpers folder.

The index.ts file is a barrel file that re-exports components from the _helpers folder so they can be imported in other files using only the folder path (e.g. './_helpers') instead of the full path to the component (e.g. './_helpers/fake-backend'). For more info on TypeScript barrel files see https://basarat.gitbooks.io/typescript/docs/tips/barrel.html.

Add the following TypeScript code to the barrel file:

export * from './fake-backend';


Add Fake Backend to App Module

Open /src/app/app.module.ts in VS Code and add the fakeBackendProvider to the providers array in the @NgModule decorator.

Angular providers tell the Angular Dependency Injection (DI) system how to get a value for a dependency. The fakeBackendProvider hooks into the HTTP request pipeline by using the Angular built in injection token HTTP_INTERCEPTORS, Angular has several built in injection tokens that enable you to hook into different parts of the framework and application lifecycle events. The multi: true option in the fakeBackendProvider tells Angular to add the provider to the collection of HTTP_INTERCEPTORS rather than replace the collection with this single provider, this allows you to add multiple HTTP interceptors to the request pipeline for handling different tasks. For more info on Angular providers see https://angular.io/guide/dependency-injection-providers.

This is how the app module file should look after adding the fakeBackendProvider, the new lines are 4-5 and 24-27.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { appRoutingModule } from './app.routing';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';

@NgModule({
    imports: [
        BrowserModule,
        appRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent,
        RegisterComponent
    ],
    providers: [
        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { };


Create Global Config Object

Open webpack.config.js and add the below global config object using webpack.DefinePlugin, this will create a global config that is accessible from anywhere in the Angular application.

The config object contains the apiUrl that will be used to make HTTP requests to the API. For more info about the webpack DefinePlugin feature see https://webpack.js.org/plugins/define-plugin/.

This is how the webpack config file should look after the update, the new lines are 1 and 23-28.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/main.ts',
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: ['ts-loader', 'angular2-template-loader']
            },
            {
                test: /\.(html|css)$/,
                use: 'raw-loader'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({ template: './src/index.html' }),
        new webpack.DefinePlugin({
            // global app config object
            config: JSON.stringify({
                apiUrl: 'http://localhost:4000'
            })
        })
    ],
    devServer: {
        historyApiFallback: true
    }
}


Create Custom Typings File

A custom typings file is used to declare TypeScript types that are created outside of the Angular application, so the TypeScript compiler is aware of them and doesn't give you compile errors about unknown types. This typings file contains a declaration for the global config object created by webpack above.

Create a file named typings.d.ts in the /src folder and add the following TypeScript declaration to it:

// so the typescript compiler doesn't complain about the global config object
declare var config: any;


Create Authentication Service

The authentication service is used to login & logout of the Angular app, it notifies other components when the user logs in & out, and allows access the currently logged in user.

RxJS Subjects and Observables are used to store the current user object and notify other components when the user logs in and out of the app. Angular components can subscribe() to the public currentUser: Observable property to be notified of changes, and notifications are sent when the this.currentUserSubject.next() method is called in the login() and logout() methods, passing the argument to each subscriber. The RxJS BehaviorSubject is a special type of Subject that keeps hold of the current value and emits it to any new subscribers as soon as they subscribe, while regular Subjects don't store the current value and only emit values that are published after a subscription is created. For more info on communicating between components with RxJS Observables see this post.

The login() method sends the user credentials to the API via an HTTP POST request for authentication. If successful the user object including a JWT auth token are stored in localStorage to keep the user logged in between page refreshes. The user object is then published to all subscribers with the call to this.currentUserSubject.next(user);.

The constructor() of the service initialises the currentUserSubject with the currentUser object from localStorage which enables the user to stay logged in between page refreshes or after the browser is closed. The public currentUser property is then set to this.currentUserSubject.asObservable(); which allows other components to subscribe to the currentUser Observable but doesn't allow them to publish to the currentUserSubject, this is so logging in and out of the app can only be done via the authentication service.

The currentUserValue getter allows other components an easy way to get the value of the currently logged in user without having to subscribe to the currentUser Observable.

The logout() method removes the current user object from local storage and publishes null to the currentUserSubject to notify all subscribers that the user has logged out.

Create Services Folder

Create a folder named _services in the /src/app folder.

The _services folder contains classes that handle all http communication with the backend API for the application, each service encapsulates the api calls for a feature (e.g. authentication) and exposes methods for performing various operations (e.g. CRUD operations, notifications etc). Services can also have methods that don't wrap http calls (e.g. authenticationService.logout()).

Create Authentication Service

Create a file named authentication.service.ts in the _services folder and add the following TypeScript code to it:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class AuthenticationService {
    private currentUserSubject: BehaviorSubject<any>;
    public currentUser: Observable<any>;

    constructor(private http: HttpClient) {
        this.currentUserSubject = new BehaviorSubject<any>(JSON.parse(localStorage.getItem('currentUser')));
        this.currentUser = this.currentUserSubject.asObservable();
    }

    public get currentUserValue() {
        return this.currentUserSubject.value;
    }

    login(username, password) {
        return this.http.post<any>(`${config.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('currentUser', JSON.stringify(user));
                this.currentUserSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem('currentUser');
        this.currentUserSubject.next(null);
    }
}


Create Barrel File in Services Folder

Create a file named index.ts inside the _services folder.

The index.ts file is a barrel file that re-exports components from the _services folder so they can be imported in other files using only the folder path (e.g. './_service') instead of the full path to the component (e.g. './_services/authentication.service.ts'). For more info on TypeScript barrel files see https://basarat.gitbooks.io/typescript/docs/tips/barrel.html.

Add the following TypeScript code to the barrel file:

export * from './authentication.service';


Import ReactiveFormsModule and HttpClientModule into App Module

There are two ways of building forms in Angular 7 - Reactive Forms or Template-Driven Forms. Reactive forms are recommended by Angular because they are more robust, scalable, reusable, and testable, so we'll be using reactive forms in this tutorial. For more info on forms in Angular see https://angular.io/guide/forms-overview.

The ReactiveFormsModule contains the components, services etc required to build reactive forms.

The HttpClientModule contains the components, services etc required to communicate with backend APIs via HTTP. For more info on sending HTTP requests in Angular see https://angular.io/guide/http.

This is how the app module file should look after adding the ReactiveFormsModule and HttpClientModule to the imports array, the new lines are 3-4 and 18-19.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { appRoutingModule } from './app.routing';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        appRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent,
        RegisterComponent
    ],
    providers: [
        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { };


Add Form Logic to Login Component

The login component contains all of the logic for validating the login form and handling form submission.

The constructor() method:

  • specifies the dependencies that are required by the component as parameters, these are automatically injected by the Angular Dependency Injection (DI) system when the component is created.
  • checks if the user is already logged in by checking authenticationService.currentUserValue and redirects to the home page if they are.

The ngOnInit() method:

  • is an Angular lifecycle hook that runs once after the component is created. For more info on Angular lifecycle hooks see https://angular.io/guide/lifecycle-hooks.
  • creates a new FormGroup by calling this.formBuilder.group() and assigns it to the this.loginForm property. The parameters passed to the FormBuilder tell it to create two form controls - username and password, the form controls are both initialised with empty strings ('') as values and set to required with the Validators.required Angular validator.
  • sets the this.returnUrl property to the value passed in the url querystring, or defaults to the home page ('/') if there isn't a value in the querystring. The return url property allows you to redirect the user back to the original page they requested before logging in.

The f() getter is a convenience property to enable shorthand access to the login form controls via this.f in the login component and f in the login component template that we'll create in the next step.

The onSubmit() method:

  • sets the this.submitted property to true to indicate that an attempt has been made to submit the form, this property is used in the login component template to display validation errors only after the first submission has been attempted.
  • checks if the form is valid by checking this.loginForm.invalid and prevents submission if it is invalid.
  • sets the this.loading property to true before submitting the user credentials via the authentication service, this property is used in the login component template to display a loading spinner to the user and disable the login button.
  • authenticates the user by calling the this.authenticationService.login() method with the username and password as parameters. The authentication service returns an Observable that we .subscribe() to for the results of the authentication. On success the user is redirected to the returnUrl by calling this.router.navigate([this.returnUrl]);. On fail the error message is stored in the this.error property to be displayed by the template and the this.loading property is reset back to false.
    The call to .pipe(first()) unsubscribes from the observable immediately after the first value is emitted.

Open the login.component.ts file and add the following TypeScript code to it:

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AuthenticationService } from '../_services'

@Component({templateUrl: 'login.component.html'})
export class LoginComponent implements OnInit {
    loginForm: FormGroup;
    loading = false;
    submitted = false;
    returnUrl: string;
    error: string;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private authenticationService: AuthenticationService
    ) {
        // redirect to home if already logged in
        if (this.authenticationService.currentUserValue) { 
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.loginForm = this.formBuilder.group({
            username: ['', Validators.required],
            password: ['', Validators.required]
        });

        // get return url from route parameters or default to '/'
        this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
    }

    // convenience getter for easy access to form fields
    get f() { return this.loginForm.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.loginForm.invalid) {
            return;
        }

        this.loading = true;
        this.authenticationService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe(
                data => {
                    this.router.navigate([this.returnUrl]);
                },
                error => {
                    this.error = error;
                    this.loading = false;
                });
    }
}


Add Form HTML to Login Component Template

The login component template contains the HTML and Angular template syntax for displaying the form in the browser and binding the form to the properties and methods in the login component above.

The [formGroup]="loginForm" attribute directive binds the form to the loginForm property of the login component. When the form is submitted in the browser the [formGroup] directive emits an ngSubmit event that triggers the onSubmit() method of the login component. The (ngSubmit)="onSubmit()" event binding attribute binds the ngSubmit event to the onSubmit() method.

Each form input has a formControlName attribute which syncs the form input with the FormControl of the same name in the loginForm.

The [ngClass]="{ 'is-invalid': submitted && f.username.errors }" attribute directive adds the is-invalid CSS class to username input if the form has been submitted and the username is invalid (contains errors) based on the validation rules defined in the login component. The same [ngClass] directive is used on the password input. The is-invalid class is part of Bootstrap 4, it makes the border of the input red to indicate it is invalid.

The div with CSS class invalid-feedback following each input is for displaying validation error messages, the messages appear as red text which are styled by Bootstrap 4. The *ngIf="submitted && f.username.errors" directive renders the username error messages if the form has been submitted and the username is invalid, the same *ngIf directive is used for password input errors. Within the invalid-feedback div is a child div for each specific error message, the *ngIf="f.username.errors.required" renders the message Username is required if the username input is empty. The Validators.required validator is attached to the username control in the login component above and has a value of true if the input is empty.

The login button is used to submit the form, the [disabled]="loading" attribute directive disables the button when the loading property of the login component is true, a spinner is also displayed in the button while loading with the use of the *ngIf="loading" directive. The spinner styling and animation is part of Bootstrap 4.

Lastly the register link uses the routerLink="/register" directive to link to the /register route. The routerLink directive uses Angular to navigate between routes using partial page updates. Don't use the standard HTML href attribute because it results in a full application reload each time you click a link.

Open the login.component.html file and add the following HTML code to it:

<div *ngIf="error" class="alert alert-danger">{{error}}</div>
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
        <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
            <div *ngIf="f.username.errors.required">Username is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
        <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
            <div *ngIf="f.password.errors.required">Password is required</div>
        </div>
    </div>
    <div class="form-group">
        <button [disabled]="loading" class="btn btn-primary">
            <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
            Login
        </button>
        <a routerLink="/register" class="btn btn-link">Register</a>
    </div>
</form>


Add Logout and Show/Hide Nav to App Component

In this step we'll update the main nav to include a logout link, and hide the nav for unauthenticated users.

Add Logic to App Component

The app component uses the authentication service to know the current logged in status and to implement logout.

The currentUser property is used to show/hide the nav when the user is logged in/out. The constructor() method subscribes to the this.authenticationService.currentUser observable and updates the currentUser when the user logs in/out.

The logout() method calls this.authenticationService.logout(); to log the user out, then redirects to the login page.

Open the app.component.ts file and add the following TypeScript code to it:

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { AuthenticationService } from './_services';

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent {
    currentUser: any;

    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) {
        this.authenticationService.currentUser.subscribe(x => this.currentUser = x);
    }

    logout() {
        this.authenticationService.logout();
        this.router.navigate(['/login']);
    }
}


Update Nav in App Component Template

The updated nav contains just two links - Home and Logout. The home link navigates to the home route ("/") using the routerLink attribute directive. The logout link calls the logout() method on click by using the (click)="logout()" event binding attribute.

The *ngIf="currentUser" directive renders the nav if the currentUser property contains a value, which indicates that the user is logged in.

Open the app.component.html file and update it with the following HTML:

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark" *ngIf="currentUser">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/">Home</a>
        <a class="nav-item nav-link" (click)="logout()">Logout</a>
    </div>
</nav>

<!-- main content container -->
<div class="jumbotron">
    <div class="container">
        <div class="row">
            <div class="col-sm-8 offset-sm-2">
                <router-outlet></router-outlet>
            </div>
        </div>
    </div>
</div>


Create Route Guard

Angular Route Guards allow you to restrict access to certain routes based on custom rules/conditions. The below route guard (AuthGuard) prevents unauthenticated users from accessing a route by implementing the CanActivate interface and defining custom rules in the canActivate() method.

When the AuthGuard is attached to a route (which we'll do shortly), the canActivate() method is called by Angular to determine if the route can be "activated". If the user is logged in and the canActivate() method returns true then navigation is allowed to continue, otherwise the method returns false and navigation is cancelled.

The canActivate() method:

  • specifies the parameters (route: ActivatedRouteSnapshot, state: RouterStateSnapshot), these are required to implement the CanActivate interface.
  • gets the value of the current user by accessing the authenticationService.currentUserValue property.
  • returns true if the currentUser contains a value, meaning that the user is logged in.
  • calls this.router.navigate() to navigate to the /login route if the user is not logged in, passing the returnUrl as a query parameter so the user can be redirected back to their original requested page after logging in.
  • returns false if the user is not logged in to cancel navigation to the current route.

Create Auth Guard

Create a file named auth.guard.ts in the _helpers folder and add the following TypeScript code to it:

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthenticationService } from '../_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUser = this.authenticationService.currentUserValue;
        if (currentUser) {
            // authorised so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
        return false;
    }
}


Add Auth Guard to Helpers Barrel File

Open the helpers barrel file (/src/app/_helpers/index.ts) and add the line export * from './auth.guard';, this enables the auth guard to be imported using only the folder path (e.g. import { AuthGuard } from './_helpers').

This is how the helpers barrel file should look after the update:

export * from './fake-backend';
export * from './auth.guard';


Add Auth Guard to Home Page Route

To control access to a route with the auth guard you add it to the canActivate array in the route's configuration. The route guards in the canActivate array are run by Angular to decide if the route can be "activated", if all of the route guards return true navigation is allowed to continue, but if any of them return false navigation is cancelled.

We'll be adding the auth guard to home page route so users so users will have to be logged in to see the home page.

Open the app routing module file (/src/app/app.routing.ts) and add canActivate: [AuthGuard] to the home page (HomeComponent) route.

This is how the app routing module file should look after the update, the new lines are 6 and 9.

import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AuthGuard } from './_helpers';

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'login', component: LoginComponent },
    { path: 'register', component: RegisterComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const appRoutingModule = RouterModule.forRoot(routes);


Create JWT Interceptor

Angular HTTP Interceptors allow you to intercept HTTP requests from your Angular app before they are sent to the backend, they can be used to modify requests before they are sent as well as handle responses.

HTTP Interceptors implement an intercept() method which is called for all requests and receives two parameters: the current request and the next handler in the chain. Multiple interceptors can be registered to handle requests, interceptors are registered in the providers section of the Angular module which we'll do shortly. An interceptor can return a response directly when it's done or pass control to the next handler in the chain by calling next.handle(request). The last handler in the chain is the built in Angular HttpBackend which sends the request via the browser to the backend. For more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor or this article.

The JWT Interceptor adds an HTTP Authorization header with a JWT token to the headers of all requests for authenticated users.

The constructor() method specifies the AuthenticationService as a dependency which is automatically injected by the Angular Dependency Injection (DI) system.

The intercept() method:

  • checks if the user is logged in by checking the authenticationService.currentUserValue exists and has a token property.
  • clones the request and adds the Authorization header with the current user's JWT token with the 'Bearer ' prefix to indicate that it's a bearer token (required for JWT). The request object is immutable so it is cloned to add the auth header.
  • passes the request to the next handler in the chain by calling next.handle(request).

Create Angular 7 JWT Interceptor

Create a file named jwt.interceptor.ts in the _helpers folder and add the following TypeScript code to it:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { AuthenticationService } from '../_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add authorization header with jwt token if available
        let currentUser = this.authenticationService.currentUserValue;
        if (currentUser && currentUser.token) {
            request = request.clone({
                setHeaders: { 
                    Authorization: `Bearer ${currentUser.token}`
                }
            });
        }

        return next.handle(request);
    }
}


Add JWT Interceptor to Barrel File

Open the helpers barrel file (/src/app/_helpers/index.ts) and add the line export * from './jwt.interceptor';, this enables the JWT interceptor to be imported using only the folder path (e.g. import { JwtInterceptor } from './_helpers').

This is how the helpers barrel file should look after the update:

export * from './fake-backend';
export * from './auth.guard';
export * from './jwt.interceptor';


Create Error Interceptor

The Error Interceptor handles when an HTTP request from the Angular app returns a error response. If the error status is 401 Unauthorized the user is automatically logged out, otherwise the error message is extracted from the HTTP error response and thrown so it can be caught and displayed by the component that initiated the request.

The constructor() method specifies the AuthenticationService as a dependency which is automatically injected by the Angular Dependency Injection (DI) system.

The intercept() method:

  • passes the request to the next handler in the chain by calling next.handle(request) and handles errors by piping the observable response through the catchError operator by calling .pipe(catchError()).
  • checks if the status code is 401 and automatically logs the user out by calling this.authenticationService.logout(). After logout the application is reloaded by calling location.reload(true) which redirects the user to the login page.
  • extracts the error message from the error response object or defaults to the response status text if there isn't an error message (err.error.message || err.statusText).
  • throws an error with the error message so it can be handled by the component that initiated the request by calling throwError(error).

Create Angular 7 Error Interceptor

Create a file named error.interceptor.ts in the _helpers folder and add the following TypeScript code to it:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthenticationService } from '../_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                // auto logout if 401 response returned from api
                this.authenticationService.logout();
                location.reload(true);
            }
            
            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}


Add Error Interceptor to Barrel File

Open the helpers barrel file (/src/app/_helpers/index.ts) and add the line export * from './error.interceptor';, this enables the Error interceptor to be imported using only the folder path (e.g. import { ErrorInterceptor } from './_helpers').

This is how the helpers barrel file should look after the update:

export * from './fake-backend';
export * from './auth.guard';
export * from './jwt.interceptor';
export * from './error.interceptor';


Add HTTP Interceptors to App Module

Open /src/app/app.module.ts in VS Code and add the JwtInterceptor and ErrorInterceptor to the providers array in the @NgModule decorator.

Angular providers tell the Angular Dependency Injection (DI) system how to get a value for a dependency. The JWT and Error interceptors hook into the HTTP request pipeline by using the Angular built in injection token HTTP_INTERCEPTORS, Angular has several built in injection tokens that enable you to hook into different parts of the framework and application lifecycle events. The multi: true option tells Angular to add the provider to the collection of HTTP_INTERCEPTORS rather than replace the collection with a single provider, this allows you to add multiple HTTP interceptors to the request pipeline for handling different tasks. For more info on Angular providers see https://angular.io/guide/dependency-injection-providers.

This is how the app module file should look after adding the JWT and Error interceptors, line 4 has been updated to import the HTTP_INTERCEPTORS injection token, and the new lines are 10 and 30-31.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { appRoutingModule } from './app.routing';
import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        appRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent,
        RegisterComponent
    ],
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { };


Start Angular 7 Application!

Run the command npm start from the project root folder (where the package.json is located) to launch the Angular 7 application.

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

 


Supported by