Securing your Spring Boot and Angular app with JWT #3 – Frontend

featured_image

There are several issues that need to be covered when you want to secure a frontend module of a Spring Boot and Angular app. Learn how to handle security based on JWT and enhance user experience with the AuthGuard functionality.

What we are going to build

In the Securing your Spring Boot and Angular app with JWT #1 – Introduction post you can find the description of the secured multi-module application which we are going to create.

In the Securing your Spring Boot and Angular app with JWT #2 – Backend post you can find the details of safeguarding the backend module.

The finished project is available in the GitHub repository – little-pinecone/jwt-spring-boot-angular-scaffolding.

In this post we are focusing on the frontend module.

To keep the code snippets reasonably short in this post, I don’t include the imports and full tests here. You can find them in the source code so don’t hesitate to inspect the repo.

Architecture overview

You can see the final directory tree for the frontend module on the image below:

Project tree - frontend screenshot

Start the Angular app

Let’s start the frontend module with the following command:

The landing page displays only the link to the Cookie dispenser page, where the cookies received from the API should be listed:

Landing page screenshot

Clicking on the Cookie dispenser link triggers the 401 error:

Cookies page 401 error screenshot

The request for cookies was send to the API, but the backend module expects a valid JWT token to grant access to the secured resources. So far, everything works as expected – the API security blocks unauthorized calls to protected endpoints.

We need to apply login functionality, obtain a valid token and attach it to every API call. Furthermore, we have to guard routes that shouldn’t be even reached without the token.

Secure the frontend

Handle the login form submission

At this moment, the application serves a simple login page with a dummy form. Whatever we insert into the username and password inputs won’t be processed:

We need to bind the credentials provided by users to the data model as in any ordinary Angular form.

Add data model

Create the credentials.ts file with the following class:

In the login.component.ts file import the Credentials class and instantiate the object with empty username and password:

Update the form template

Bind both inputs with [(ngModel)] and provide validation messages. Below is the code for the username input:

Then copy the following code for the password input:

Disable the Login button until the form is completed correctly. You also want to call the login() function when the form is submitted, therefore add the following code to the view:

We have to add the login() function to the login.component.ts file:

At this moment the method is empty, we will implement the login functionality later.

As a result, the form submission is turned off until the credentials are provided. In the screenshot below you can see that removing the username value produces the validation message for that field:

Invalid login form screenshot

The work done in this section is contained in the commit c9be87c3acae01148703bbcd449026eb90bccd79.

Send a login request

We need to call the API to authenticate a user who tries to log in. In case of a successful authentication, the backend sends the Authorization header containing the JWT token that we need to extract, store and append to every subsequent API call.

Create the token service with the following command:

We will use it to call the API login endpoint. Copy the following code to the service:

The apiUrl value is already defined in the environment.ts and environment.prod.ts files:

Adjust the apiUrl value to your development and production configuration.

Obtain the token

We created the service that sends the login requests. In the next step we are going to provide the logic for extracting the right header from the response.

Create the authorization service:

We are going to define the key that will be used for storing the JWT token in the fronted module. What’s more, after users are authenticated, we want to redirect them from the '/login' path. The redirectToUrl variable is set to '/cookies' by default, but will carry any path that user wanted to reach before logging in to the application. Of course, we need to inject the TokenService to call the API and Router to handle redirections.

Copy the following code to the service:

Furthermore, we are going to add the core responsibility – the login() function. It calls the getResponseHeaders from the TokenService and saves the token from the Authorization header in the LocalStorage. Add the following code to the auth.service.ts file

We also need a method to reach for the currently stored token, so add this last function to the file:

The authorization service is ready to be used in the app.

Implement the login function

Let’s get back to our empty login() function in the login.component.ts file. We are going to inject the AuthService to the constructor so we can use it inside the login() method :

After those changes, submitting the login form with legitimate credentials will result in receiving a valid token from the API that will be stored in the LocalStorage. A user is authenticated in the app and is automatically redirected to the "/cookies" page. However, the token is not added to this request – instead of the page with tasty treats we are getting the 401 error. We are going to deal with this issue in the next section.

The work done in this section is contained in the commit 71b13383b4b77819234b854e97fec419c60bc450.

Append the token to every API request

We need to intercept every request to the API and add the token from the LocalStorage. Create the jwt.token.interceptor.ts file and copy the following code to it:

We are injecting the AuthService to access the getToken() function. Then, we are appending the Bearer token to any request send to the API.

We have to declare usage of this interceptor in app.module:

Refresh the page and you will see the cookies served by the API.

The work done in this section is contained in the commit e561ff512771a06404cda39e293432429a691c5e.

Add logout functionality

The API security configuration allows us to call the logout endpoint:

Let’s use it and add the logout() function to the token.service file:

After a successful API call we can remove the token from the LocalStorage. We will also need a function to check the current state of a user. Copy both methods to your auth.service file:

To display the Login or Logout option in the application header we need to inject the AuthService into the HeaderComponent and implement the logout() function:

We are going to display the Login or Logout option depending of the current state of a user returned by the authService.isLoggedIn() method:

We need to declare the hidden class in the file for the header styles:

The header on your login page should look like the one on the screenshot below:

Login page header screenshot

The work done in this section is contained in the commit bc5a317d7606e8ae56b4df4580f8988e1c3ef1d4.

Implement AuthGuard

Our API will return 401 error to the web console when unauthorized users try to access any secured route. That’s not a user friendly solution. We should redirect those users to the login page instead of sending requests without tokens. We are going to add AuthGuard to enhance the usability of our application.

Generate the guard with the following command:

The guard will use AuthService to verify if users are logged in and Router to redirect them to the login form if they are not. Let’s inject them to the constructor:

We will replace the default code in the canActivate() function with the following lines:

We are declaring the url variable which holds the path that the user tried to access. Depending on the boolean returned by the checkLogin() method the access to that path will be granted or not. Copy the function to the file:

The last thing that needs to be done is to declare paths that are guarded. Add the canActivate: [AuthGuard]  part to your routing module:

The page available for the authenticated and authorized users should look like the one on the screenshot below:

cookie-dispenser-screenshot

The work done in this section is contained in the commit f9d82ce0c3c4ee26b058c374f999df3f360a0d65.

Handle 401 errors returned from the backend

Right now, when an unauthorised user tries to reach the protected /cookies route, the Angular AuthGuard blocks sending a request to the backend and the user is redirected to the /login page. However, when we have a route that is not guarded explicitly by Angular, the request will be sent to the backend and the user will receive a 401 error.

In our example we don’t deal with expired tokens. Therefore, if the token is no longer valid and the user wants to log out, the unauthorised request will be sent to the backend. Right now the frontend does not handle properly the API response shown in the screenshot below:

Postman - unauthorised logout

We can manage this situation by adding error handling in the interceptor:

We created the handleErrors function that redirects users to the /login page when the API returns 401 error and we use it when the intercepted request is returned. To achieve this we had to inject the Router instance in the constructor.

The work done in this section is contained in the commit e26dbc7649a0bb48bbf0f2f66ae32179490d120a.

Photo by Yingchou Han on StockSnap

4 thoughts on “Securing your Spring Boot and Angular app with JWT #3 – Frontend

  1. WOW! All 3 of your tutorials are THE BEST tutorials I encountered on the internet! Thanks for all the effort you put into this!

    1. Thank you 🙂 Recently I realised that I missed handling 401 errors from the backend so I added it to the tutorial (“Handle 401 errors returned from the backend” section) and to the repository (this commit).

  2. It’s not clear to me how your logout is working. When I hit /logout I’m getting a 404. I don’t see anywhere in your config where you setup the logout URL as api/logout or how you’re handling the logout functionality.

  3. Hi,

    We are going to explore files in the following order:

    header.component.html

    header.component.ts

    auth.service.ts

    token.service.ts

    SecurityConfig.java

    When I click ‘Logout’ link in my header.component.html, I call the logout() function from the header.component.ts that calls the logout() function from the authService.

    Two things happen there (auth.service.ts):

    1. logout() function from the tokenService is called.

    2. Token is removed from the localStorage.

    So we go to the token.service.ts file. The logout() function sets the logoutUrl variable. It’s API_URL + ‘logout’. I set API_URL in the environment.ts file to ‘http://localhost:8080/api’. Therefore, the whole url is ‘http://localhost:8080/api/logout‘.

    After setting the url, the authService uses it to send a get request to the backend:

    return this.http.get(logoutUrl, {responseType: 'text'});

    There is a test in the token.service.spec.ts file: ‘should call logout endpoint’.

    In SecurityConfig I used the logout() functionality in the configuration (line 46). And Spring Boot deals with logging out the user for me.

    However, as I’m reading the docs for HttpSecurity.java, I see that adding the logout to the configuration seems to be redundant as the

    LogoutConfigurer<HttpSecurity> logout()

    “(…) is automatically applied when using WebSecurityConfigurerAdapter.” and my SecurityConfig extends WebSecurityConfigurerAdapter.

    All in all, if info that the logout link was clicked is transferred through header.component.ts, auth.service.ts to token.service.ts (where the backend is finally called) logging out works.

    The process is documented in the ‘Add logout functionality section‘ of this post. You can also find the relevant code in the bc5a317d7606e8ae56b4df4580f8988e1c3ef1d4 commit.

    I hope it helps 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *