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:
- Part 1 - Create Base Project Structure
- Part 2 - Add Routing & Multiple Pages
- Part 3 - Login Form, Authentication & Route Guard
- Part 4 - Registration Form & Service Methods
- Part 5 - Alerts & Home Page
- Part 6 - User Management (CRUD) Section
- Part 7 - Migrate to Standalone Components and Functional Interceptors
- Part 8 - Dockerize App with Nginx
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
- Create Fake Backend API
- Create Angular Environment File
- Create User Model
- Update TS Config File
- Create Account Service
- Import ReactiveFormsModule and HttpClientModule into App Module
- Add Form Logic to Login Component
- Add Form HTML to Login Component Template
- Add Logout and Show/Hide Nav to App Component
- Create Route Guard
- Create JWT Interceptor
- Create Error Interceptor
- Add HTTP Interceptors to App Module
- 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 callingthis.formBuilder.group()
and assigns it to thethis.form
property. The parameters passed to theFormBuilder
tell it to create two form controls -username
andpassword
, the form controls are both initialised with empty strings (''
) as values and set to required with theValidators.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, theloading
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 theusername
andpassword
as parameters. The account service returns anObservable
that we.subscribe()
to for the results of the authentication. On success the user is redirected to thereturnUrl
by callingthis.router.navigateByUrl(returnUrl);
. On fail the error message is stored in thethis.error
property to be displayed by the template and thethis.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 theCanActivate
interface. - gets the value of the current user by accessing the
accountService.userValue
property. - returns
true
if the currentuser
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 thereturnUrl
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 atoken
property. - checks if the request is to the API URL.
- clones the
request
and adds theAuthorization
header with the current user's JWT token with the 'Bearer ' prefix to indicate that it's a bearer token (required for JWT). Therequest
object is immutable so it is cloned to add the auth header. - passes the
request
to the next handler in the chain by callingnext.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 callingnext.handle(request)
and handles errors by piping the observable response through thecatchError
operator by calling.pipe(catchError())
. - checks if the status code is
401
or403
and automatically logs the user out by callingthis.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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!