Full Stack Development Series Part 9: User Authentication and JWT Support in Angular
Learn how to add JWT support to your Angular-based web app! Part of our full-stack software project series.
As you know, user authentication is a crucial aspect of any web application, and in our previous blog posts, we added it to the backend API. However, to ensure complete security, we also need to add authentication and authorization to the frontend using JSON Web Tokens (JWTs). In this post, we'll guide you through the process of adding JWT support to your Angular-based web app, enabling you to create a secure and efficient full-stack software project. So, let's get started!
If you'd like to skip ahead to the code for this post, check it out here: wgd3/full-stack-todo@part-09
Goals
- Create a login page
- Handle login/logout functionality
- Create a user registration page
Create A Login Page
Following the pattern of "feature modules," our first step is to use the Nx generator for a new login feature:
$ npx nx generate @nrwl/angular:library FeatureLogin \
--directory=libs/client \
--changeDetection=OnPush \
--importPath=@fst/client/feature-login \
--skipModule \
--standalone \
--style=scss \
--tags=type:feature,scope:client
We'll be using Storybook to design this page, so next let's add Storybook support to the library:
$ npx nx generate @nrwl/angular:storybook-configuration client-feature-login \
--tsConfiguration \
--configureTestRunner
The last bit of configuration is in the library's project.json
where the build-storybook
target gets updated to utilize our style library:
After the library is configured and files are in place, we can use some basic placeholder code to start designing. You'll notice that this HTML does not contain anything Angular-specific. That is intentional, as we're going with a design-first approach to this page.
<div class="login__container">
<div class="login__header">
<h1>Login To Full Stack To Do</h1>
</div>
<div class="login__text">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam similique
ducimus id, adipisci eveniet cum facere enim, sint delectus voluptate
reprehenderit rerum fugit vitae illo! Quis cupiditate eum dignissimos
sint?
</p>
</div>
<div class="login__form-container">
<form>
<div class="input-group">
<label class="form-label">Email</label>
<input type="email" class="form-control" />
</div>
<div class="input-group">
<label class="form-label">Password</label>
<input type="password" class="form-control" />
</div>
<button type="submit" class="btn btn--primary">Submit</button>
</form>
</div>
</div>
I made a small change to the story for this component to make visualization a little easier:
title: 'Login Component',
component: ClientFeatureLoginComponent,
decorators: [
componentWrapperDecorator(
(s) => `
<div style="width: 50vw; height: 100vh">${s}</div>
`
),
],
Initially, this template doesn't look too bad! Thanks to our CSS classes from the style library, we don't need to add much other than some layout styles.
Handling JWTs and User API Calls
In order for the login page to be functional, we need to create a service to interact with the backend and generate JWTs. We should also create a User service to get information about a user.
# create user service
$ npx nx generate @schematics/angular:service User \
--project=client-data-access \
--path=libs/client/data-access/src/lib
# then create JWT/Auth service
$ npx nx generate @schematics/angular:service Auth \
--project=client-data-access \
--path=libs/client/data-access/src/lib
# add a utility library to hold client-wide constants
$ npx nx generate @nrwl/node:library util \
--directory=libs/client/ \
--importPath=@fst/client/util \
--strict \
--tags=type:util,scope:client \
--unitTestRunner=none
# install jwt-decode so we can read tokens on the client side
$ npm i jwt-decode
I'm going to illustrate a way to store any received tokens on the client side, however you should be aware that this is a somewhat controversial topic. Check out this article for more information: LocalStorage vs Cookies.
We're going to need a key to reference when storing tokens; let's add a constant to our new utility library:
ApiService
to TodoService
. It was clear that this service was used only for To-do API calls, and I didn't want a name like ApiService
to obfuscate that. The authentication service we made has a few specific responsibilities: handle/store JWTs and make the HTTP API calls for logging in users. You can see below how when a JWT is received it is both stored in LocalStorage
and in a BehaviorSubject
. The reason it goes to LocalStorage
is so that when a user refreshes the page, or closes the window and returns later, the JWT can be retrieved without forcing the user to log in again.
Next we're going to add an Interceptor which will be responsible for adding the JWT to each outgoing API request. You could manually add the Authorization
header to each HTTP request in the various services, but that results in repeated code and services that rely on each other.
$ npx nx generate @schematics/angular:interceptor Jwt \
--project=client-data-access \
--functional \
--path=libs/client/data-access/src/lib/interceptors
This interceptor will not be class-based service (as was the convention for some time) - it's going to be a simple function!
Our interceptor needs to be provided to HttpClient
during application bootstrap:
Lastly, with the entire authorization infrastructure in place, we can use a Guard to prevent access to certain pages:
$ npx nx generate @schematics/angular:guard Auth \
--project=client-data-access \
--functional \
--path=libs/client/data-access/src/lib/guards
To apply the guard to the dashboard, update the application's routes:
Make The Login Page Functional
With our template in place and styles applied we can move onto making the component functional! This component will utilize a FormGroup
to create a strongly-typed, reactive form and handle validation.
Reactive forms have excellent support for validation criteria, and is separate from any HTML attributes that may be specified in a template. Since we have made both form fields required, let's link the FormGroup
to the template and add some validation styles:
Now when a user successfully logs in we can use Angular's router to automatically load the user's dashboard:
submitForm() {
if (this.loginForm.valid && this.loginForm.dirty) {
const { email, password } = this.loginForm.getRawValue();
this.authService
.loginUser({ email, password })
.pipe(take(1))
.subscribe({
next: () => {
this.router.navigate(['/']);
},
error: (err) => {
console.error(err);
},
});
}
}
Handling Login Errors
Separate from our client-side form validation is the error handling of HTTP responses. If a user enters the wrong password, or the user doesn't exist, we need to provide visual feedback that there was an error.
I chose to use a BehaviorSubject
in the component to store this message if the need arises:
errorMessage$ = new BehaviorSubject<string | null>(null);
The submitForm
method was also updated with error-handling code:
On the template side, we add a paragraph element with an ngIf
directive to conditionally render this message:
Adding Logout Functionality
Our users can now log in! That's excellent, but now there needs to be a way to handle logging out. Since our header is a logical place for a "Log Out" link, and the header is part of the AppComponent
, let's add a simple logout()
method and wire it up to the template:
<div class="header__actions">
<ng-container *ngIf="user$ | async as user; else login">
<span class="nav-link">Hello, {{ user.email }}!</span>
<span class="nav-link" (click)="logout()">Log Out</span>
</ng-container>
<ng-template #login>
<a href="#" class="nav-link" [routerLink]="['/login']">Log In</a>
</ng-template>
</div>
User's browser storage will no longer have the JWT and they will be immediately redirected to the login page.
Adding A Register Page
Last but not least is the registration page - we want users to be able to use the application! We're going to follow the same steps as the login page: create a new feature library, add Storybook support, and add supoprt for our style library:
$ npx nx generate @nrwl/angular:library FeatureRegister \
--directory=libs/client \
--routing \
--changeDetection=OnPush \
--flat \
--importPath=@fst/client/feature-register \
--simpleName \
--skipModule \
--standalone \
--style=scss \
--tags=type:feature,scope:client
Add storybook support for design
$ npx nx generate @nrwl/angular:storybook-configuration client-feature-register --configureTestRunner
build-storybook
run target in the new library!At this point I copied the template code from the login page for the registration template as a starting point. After adding some layout styles and adjusting for the different form, I ended up with this:
Everything is functionally the same as the login form - with one small exception I wanted to cover. Custom form validators!
We could use the FormGroup's observable valueChanges
to dynamically compare the values of the two password fields or... use the reactive form's built-in support for custom validation rules. The above validator gets passed the name of 2 FormControl
objects, compares their values, and if they don't match, returns a ValidationErrors
object.
Summary
Our Angular app now supports JWT authentication, providing your users a secure and efficient experience. By following this guide, you have learned how to implement end-to-end authentication across both of these applications. As always, you can check out the source code for this project on my GitHub repository: wgd3/full-stack-todo@part-09
Stay tuned for our next post in this series, where we'll dive deeper into developing the application's features. Thanks for reading, and happy coding!
Speed Bumps
- Strange double API call on login.
JwtInterceptor
was watching changes toaccessToken$
so every time it was triggered a request would be duplicated! Troubleshooting: canceled request was coming from line 39 of authservice, which just callednext()
on the accessToken$. Looked for all references to that observable, found theJwtInterceptor
ref, fixed by addingtake(1)
304 Not Modified
responses - just indicates nothing's changed! https://wanago.io/2022/01/17/api-nestjs-etag-cache/- Angular test coverage was complaining that constructors were not being covered by tests. Turns out
emitDecoratorMetadata
is no longer required, but moving to injectables also solves the issue. https://github.com/nrwl/nx/issues/12132#issuecomment-1268408588