Published: May 02 2023
Last updated: May 11 2023

Angular 15/16 Free Course #3 - Login Form, Authentication & Route Guard

Built and tested with Angular 15 and Angular 16

In this free step by step Angular course we'll be covering how to implement routing, authentication, registration and CRUD functionality in Angular.

Other parts available in this Angular course:


Angular Tutorial Part 3

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

Code on GitHub

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


Tutorial Steps

  1. Create Fake Backend API
  2. Create Angular Environment File
  3. Create User Model
  4. Update TS Config File
  5. Create Account Service
  6. Import ReactiveFormsModule and HttpClientModule into App Module
  7. Add Form Logic to Login Component
  8. Add Form HTML to Login Component Template
  9. Add Logout and Show/Hide Nav to App Component
  10. Create Route Guard
  11. Create JWT Interceptor
  12. Create Error Interceptor
  13. Add HTTP Interceptors to App Module
  14. Start Angular Application!


Create Fake Backend API

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.

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, account, users), 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, 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;

        return handleRoute();

        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({
                ...basicDetails(user),
                token: 'fake-jwt-token'
            })
        }

        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500)); // delay observable to simulate server api call
        }

        function error(message: string) {
            return throwError(() => ({ error: { message } }))
                .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
        }

        function basicDetails(user: any) {
            const { id, username, firstName, lastName } = user;
            return { id, username, firstName, lastName };
        }
    }
}

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.gitbook.io/typescript/main-1/barrel.

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

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.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';

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


Create Angular Environment File

The environment file will export a global environment config object that is accessible from anywhere in the Angular application. The environment object contains the apiUrl that will be used to make HTTP requests to the API.

Create Environments Folder

Create a folder named environments in the /src folder.

Create Environment File

Create a file named environment.ts in the /src/environments folder and add the following TypeScript code to it:

export const environment = {
    apiUrl: 'http://localhost:4000'
};


Create User Model

The user model is a small class that defines the properties of a user in the Angular application.

Create Models Folder

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

Create User Model Class

Create a file named user.ts in the _models folder and add the following TypeScript code to it:

export class User {
    id?: string;
    username?: string;
    password?: string;
    firstName?: string;
    lastName?: string;
    token?: string;
}


Create Barrel File in Models Folder

Create a file named index.ts inside the _models folder and add the following TypeScript code to it:

export * from './user';


Update TS Config File

We're going to make two changes to the TypeScript Config file:

  • Add path aliases
  • Disable the rule noPropertyAccessFromIndexSignature

Path Aliases

Path aliases @app and @environments are configured in the TypeScript config (/tsconfig.json) to map to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent').

Add the following "paths" config to the "compilerOptions" in the /tsconfig.json file:

"paths": {
    "@app/*": ["src/app/*"],
    "@environments/*": ["src/environments/*"]
}


TypeScript rule noPropertyAccessFromIndexSignature

This rule is set to true by default and gives a warning when trying to access a form control with dot syntax (e.g. this.f.username.value).

To disable it set noPropertyAccessFromIndexSignature to false in the /tsconfig.js file:

"noPropertyAccessFromIndexSignature": false


The updated TypeScript config file should look like this with both changes:

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "ES2022",
        "module": "ES2022",
        "useDefineForClassFields": false,
        "lib": [
            "ES2022",
            "dom"
        ],
        "paths": {
            "@app/*": ["src/app/*"],
            "@environments/*": ["src/environments/*"]
        }
    },
    "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
    }
}


Create Account Service

The account 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

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 user: Observable property to be notified of changes, and notifications are sent when the this.userSubject.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 Angular 14 - Communicating Between Components with RxJS Observable & Subject.

Service Methods and Properties

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.userSubject.next(user);.

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

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

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

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. accounts) and exposes methods for performing various operations (e.g. authentication, CRUD operations etc). Services can also have methods that don't wrap http calls (e.g. accountService.logout()).

Create Account Service

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

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

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AccountService {
    private userSubject: BehaviorSubject<User | null>;
    public user: Observable<User | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
        this.user = this.userSubject.asObservable();
    }

    public get userValue() {
        return this.userSubject.value;
    }

    login(username: string, password: string) {
        return this.http.post<User>(`${environment.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('user', JSON.stringify(user));
                this.userSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem('user');
        this.userSubject.next(null);
        this.router.navigate(['/account/login']);
    }
}


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. './_services') instead of the full path to the component (e.g. './_services/account.service.ts'). For more info on TypeScript barrel files see https://basarat.gitbook.io/typescript/main-1/barrel.

Add the following TypeScript code to the barrel file:

export * from './account.service';


Import ReactiveFormsModule and HttpClientModule into App Module

ReactiveFormsModule

There are two ways of building forms in Angular - 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.

HttpClientModule

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 17-18.

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.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';

@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 accountService.userValue 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.form 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.

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.
  • resets the error message to an empty string to remove any alert.
  • checks if the form is valid by checking this.form.invalid and prevents submission if it is invalid.
  • sets the this.loading property to true before submitting the user credentials via the account service, the loading 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.accountService.login() method with the username and password as parameters. The account 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.navigateByUrl(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 { AccountService } from '@app/_services'

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

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

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

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

    onSubmit() {
        this.submitted = true;

        // reset alert on submit
        this.error = '';

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

        this.loading = true;
        this.accountService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    // get return url from query parameters or default to home page
                    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                    this.router.navigateByUrl(returnUrl);
                },
                error: 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]="form" attribute directive binds the form to the form 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 form.

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 5, 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 5. 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 5.

Lastly the register link uses the routerLink="../register" directive to link to the /account/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 class="container col-md-6 offset-md-3 mt-5">
    <div *ngIf="error" class="alert alert-danger">{{error}}</div>
    <div class="card">
        <h4 class="card-header">Login</h4>
        <div class="card-body">
            <form [formGroup]="form" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">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="mb-3">
                    <label class="form-label">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>
                    <button [disabled]="loading" class="btn btn-primary">
                        <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                        Login
                    </button>
                    <a routerLink="../register" class="btn btn-link">Register</a>
                </div>
            </form>
        </div>
    </div>
</div>


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 account service to know the current logged in status and to implement logout.

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

The logout() method calls this.accountService.logout(); to log the user outand redirect to the login page.

Open the app.component.ts file and update it with the following TypeScript code:

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

import { AccountService } from './_services';
import { User } from './_models';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    user?: User | null;

    constructor(private accountService: AccountService) {
        this.accountService.user.subscribe(x => this.user = x);
    }

    logout() {
        this.accountService.logout();
    }
}


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 button link calls the logout() method on click by using the (click)="logout()" event binding attribute.

The *ngIf="user" directive renders the nav if the user 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 px-3" *ngIf="user">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/">Home</a>
        <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container">
    <router-outlet></router-outlet>
</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 accountService.userValue property.
  • returns true if the current user contains a value, meaning that the user is logged in.
  • calls this.router.navigate() to navigate to the /account/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 { AccountService } from '@app/_services';

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

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const user = this.accountService.userValue;
        if (user) {
            // authorised so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/account/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 './auth.guard';
export * from './fake-backend';


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.module.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 updated lines are 6 and 9.

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

import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';
import { AuthGuard } from './_helpers';

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

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

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


Create JWT Interceptor

The JWT Interceptor adds an HTTP Authorization header with a JWT token to the headers of all requests to the API URL if the user is authenticated.

Angular HTTP Interceptors

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.

JWT Interceptor Details

The constructor() method specifies the AccountService 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 accountService.userValue exists and has a token property.
  • checks if the request is to the API URL.
  • 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 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 { environment } from '@environments/environment';
import { AccountService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if user is logged in and request is to the api url
        const user = this.accountService.userValue;
        const isLoggedIn = user?.token;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: { Authorization: `Bearer ${user.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 './auth.guard';
export * from './fake-backend';
export * from './jwt.interceptor';


Create Error Interceptor

The Error Interceptor handles when an HTTP request from the Angular app returns a error response.

Auto logout on 401 or 403

If the error status is 401 Unauthorized or 403 Forbidden the user is automatically logged out because this indicates that the JWT token is no longer valid. Otherwise the error message is extracted from the HTTP error response and re-thrown so it can be caught and displayed by the component that initiated the request.

Error Interceptor Details

The constructor() method specifies the AccountService 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 or 403 and automatically logs the user out by calling this.accountService.logout().
  • 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 the error message (throwError(() => error);) so it can be handled by the component that initiated the request.

Create Angular 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 { AccountService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status) && this.accountService.userValue) {
                // auto logout if 401 or 403 response returned from api
                this.accountService.logout();
            }

            const error = err.error?.message || err.statusText;
            console.error(err);
            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 './auth.guard';
export * from './error.interceptor';
export * from './fake-backend';
export * from './jwt.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

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.

Add Providers to App Module

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 29-30.

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.module';
import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';

@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 Application!

Run the command npm start from the project root folder (where the package.json is located) to start the Angular application, then open a browser tab to the URL http://localhost:4200.

Login with test user

You can login with the test user configured in the fake backend API - username: test password: test

The command output should look something like this:

PS C:\Projects\angular-tutorial> npm start

> [email protected] start
> ng serve

✔ Browser application bundle generation complete.

Initial Chunk Files   | Names         |  Raw Size
vendor.js             | vendor        |   2.44 MB |
polyfills.js          | polyfills     | 314.29 kB |
styles.css, styles.js | styles        | 209.42 kB |
main.js               | main          |  39.62 kB |
runtime.js            | runtime       |   6.54 kB |

                      | Initial Total |   2.99 MB

Build at: 2023-05-02T00:23:10.484Z - Hash: 306e601de4b654fa - Time: 7559ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **


√ Compiled successfully.

 


Need Some Angular 16 Help?

Search fiverr for freelance Angular 16 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by