Securing your Spring Boot and Angular app with JWT #2 – Backend

featured_image

In this article you can read about applying Spring Security to the backend module of a Spring Boot and Angular app. Check out how to configure security, generate JWT tokens and protect API endpoints.

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 #3 – Frontend post you can find the details of safeguarding the frontend 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 backend 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 backend module on the image below:

Project tree - backend screenshot

Get the base project

To get the code of the release 0.1.1  from the jwt-spring-boot-angular-scaffolding repository you can clone the project with this command:

It’s the starting point for securing the Cookie dispenser application.

Secure the backend

Right now all calls to the API are allowed, as you can see in this screenshot taken from Postman:

Postman - a successful call to the not secured endpoint

When we are done, the path http://localhost:8080/cookies will be available only for authenticated and authorized users.

Enable Spring Security

Add the dependency

Add the Spring Security dependency to the backend pom file:

After rebuilding the project, all calls to the API should fail:

Postman - a failed call to the secured endpoint

We’re getting the 401 Unauthorized error so the default security configuration works as intended.

Fix tests after enabling Spring Security

The CookieController has a test that won’t be executed properly after adding the security. We are getting the following error:

To fix it, add the spring-security-test dependency to the backend pom file:

Furthermore, we need to add the @WithMockUser(roles = "USER") annotation to the test:

After those changes the test execution should run without any further errors.

The work done in this section is contained in the commit 1d6b7c76360d2ad98ec02c1af0c63719dd29c87c.

Handle user registration

Create the model for User data

To create the User class, annotated as a JPA entity, we need the spring-boot-starter-data-jpa dependency added to our project:

Create the User class and copy the following code to the file:

The User entity has fields for an id and the embedded UserCredentials class. We need getters for the properties and two constructors. Methods: eguals(), hashCode() and toString() are omitted for the sake of brevity, don’t forget to generate them in your project.

Next we are going to add the class for username and password – create the UserCredentials class and copy the following code into it:

Again, eguals(), hashCode() and toString() are omitted in the code snippet above.

Create the UserRepository interface that extends CrudRepository to manage the persistence layer:

Provide business logic for handling users

The logic for creating a new user is enclosed in the UserService – we need to encrypt passwords, give every user a default role in the app and save users in a database. Copy the following code to your service:

To store encrypted passwords we need to create an encoder that is injected into the UserService. Create a new configuration file – PasswordEncoderConfig and declare the encoder bean in it, like in the following code snippet:

Last but not least, create a REST controller to handle the registration requests. Copy the code from the UserController below:

This endpoint enables new users to register by using the UserService we provided.

Configure security

We need to override the default security configuration to specify which requests require authorization and how to handle forbidden API calls. In our case, we also need to provide the possibility to apply CORS configuration depending on the environment. Create the SecurityConfig file and copy the following code:

All API requests, apart from the one for user registration ( .antMatchers(HttpMethod.POST, "/api/users").permitAll() ), are secured.

If we decide to serve something on paths other than http://localhost:8080/api/**  (e.g. a landing page on http://localhost:8080/ ) the authentication won’t be required.

In case of not authorized calls to the API, the server will return a response with 401 status and “Unauthorized” error.

We use @Value("${cors.enabled:false}")  to disable CORS on the production environment. To change it during development, we need to create the application-development.properties file and copy the following property there:

Now we can test the request creating a new user:

Postman - registering a user

 

The work done in this section is contained in the commit 40e2d0ec5745cb0605a5c59525b8628cf2df0dc7.

The commit contains also all tests for UserController and UserService, remember to include them in your code.

Add JWT

Include the dependency and define token properties

Start with adding the dependency for JWT:

We will define token properties and initialize them with the following default values in the TokenProperties component:

Remember to annotate it with @ConfigurationProperties to bind the external configuration to this class so you can set your own expiration time and secret in the production environment:

Enable searching for a user during the authentication process

In the UserRepository interface declare the method for getting a user by a username:

And use it in the UserService class.

We are going to use our implementation of findByUsername() function during authentication. In order to achieve that we need to create the UserDetailService interface implementation, override its loadUserByUsername() function and inject our UserService to access the method we’ve created above.

Copy the following code to enable fetching a UserDetails object with complete credentials and granted authorities. In case of requesting for a not existing user a UsernameNotFoundException is thrown:

Add AuthenticationFilter

Create the AuthenticationFilter class that extends UsernamePasswordAuthenticationFilter (Spring Boot will automatically place our filter in the most suitable position in the Security Filter Chain) and initialize all essential properties in its constructor:

Now we can override the attemptAuthentication() function that is called whenever the API gets a request to the login path. Let’s extract the credentials sent in a request and use it to generate the UsernamePasswordAuthenticationToken instance that we pass to the authenticationManager:

We will also override the successfulAuthentication() function to add the Authorization header with the JWT token data to the response. The header is prefixed with “Bearer ” from the TokenProperties configuration and contains the complete token built with the Jwts.builder(). The createToken() function is also the place where you can specify the signing algorithm. Copy the following code to the AuthenticationFilter.java file:

Add AuthorizationFilter

Create the AuthorizationFilter class that extends OncePerRequestFilter and inject the TokenProperties instance to it:

We need to override the doFilterInternal() function to extract the Authorization header from a request. Let’s stop for a moment and investigate the logic here. If the Authorization header is found we verify whether a user that sent the request is authorized to access the content:

First we parse the token after removing the “Bearer ” prefix from it:

If the claims subject (principal) is present we can set the user context for the authorized user:

And finally we have to pass the control to the next filter from the filterChain:

Update the security configuration

Change the class annotation to @EnableWebSecurity, add the following properties and constructor to the code:

To the configure() method we are going to add:

  • logout() functionality – line 11;
  • AuthenticationFilter – line 13;
  • AuthorizationFilter – line 14;
  • a matcher to the login path that is available for all users – line 16;
  • a matcher to restrain all requests (other than registration) that are related to the "/api/users/**" endpoint to be accessible only to a user with the ADMIN role – line 18.

The method should look like the one on the code snippet below:

Allow usage of your custom userDetailService with BCryptPasswordEncoder:

Check out the login functionality

Rebuild the application, register a test user and test the login functionality:

Postman - loggin in a user

 

Copy the token value from the Authorization header, and use it to display the secured pastry – change the Authorization Type to the Bearer Token and paste the token value:

Postman - a successful call to the secured endpoint

Fix the tests

When we try to run tests, e.g. for the UserController class, we will get the following error in the console:

We need to add the dependencies we already injected to the SecurityConfig and the config class itself.

Update UserControllerTestwith the @Import() annotation:

Update CookieControllerTestwith the @Import() annotation and inject a Mock for the UserService:

The work done in this section is contained in the commits 6b4c29c6c11ca0cf435203475cde4f6d1809c6bf. The necessary fixes are added in the commits: 1b80e70533f4823086c7aefb556884452aa311e9 and 0eba82766d98ca5c242d8a07467139a0dab3038b.

Photo by Matheus Bertelli on StockSnap

4 thoughts on “Securing your Spring Boot and Angular app with JWT #2 – Backend

  1. Hi,

    I got problem with logout function, can you help me?

    java.io.FileNotFoundException: class path resource [static/index.html] cannot be resolved to URL because it does not exist

    at org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:195) ~[spring-core-5.1.3.RELEASE.jar:5.1.3.RELEASE]

    at org.springframework.core.io.AbstractFileResolvingResource.lastModified(AbstractFileResolvingResource.java:247) ~[spring-core-5.1.3.RELEASE.jar:5.1.3.RELEASE]

    at org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(ResourceHttpRequestHandler.java:467) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

    1. Hi,

      If you are building a project on your own, remember to surrender the routing control to Angular. Check out this post – Make Spring Boot surrender routing control to Angular. This feature is already implemented in this project and the configuration is in the MvcConfiguration file.

      You can also check out these two commits:

      * ea11fb6a53b3b1575673669d76fd0bb2f65f4c0e

      * 3cd0850b40798fbacc93d955f81938d8a8aa31fc (small bugfix)

      Maybe your project is missing something that is not described in this post (I wanted to keep it as concise as possible)?

      You can always clone the whole project and checkout to the commit where login and logout functionality works:

      $ git checkout 6b4c29c6c11ca0cf435203475cde4f6d1809c6bf

      It contains most of the code described in this post.

      Logout requires a valid jwt token (otherwise it will result in 401 error).

      All in all, make sure that Spring Boot surrenders routing control to Angular and you have no issues with CORS (Fix CORS issues between Spring Boot and Angular on localhost).

      I hope you will fix the issue in no time 🙂

      Regards, little_pinecone

  2. Hi Marta,

    I had solved my issue by way remove  the token in Local Storage and redirect to login page 🙂 But now I have another problem called refresh or update expired JWT, can you point me or create a guide how to refresh token? Thank you so much!

    1. Thank you for sharing the issue and the solution 🙂 Regarding the token refreshing, I haven’t quite figured it out yet but it’s on my todo list. “Silent authentication” is a way to solve this issue but it is a complex subject and I haven’t found an existing tutorial/resources that would work on my JWT-Spring Boot-Angular example project. As soon as I have a satisfying solution, I will share it.

Leave a Reply

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