DEV Community 👩‍💻👨‍💻

charles1303
charles1303

Posted on

Security Using Spring and JWT

Introduction

I've finally been able to get this article out. It took this long because of two major reasons; firstly I was enmeshed in a very interesting project over the past few weeks. It really felt good when we deployed it, did a test run and saw that all the stakeholders were happy about how the product met the expected criteria and the desired features. Secondly and on the sad side, my hard disk crashed and the recovery process was quite daunting. (I'm still taking stock of what I've lost so far especially my write ups and documentations). But thankfully most of my project codes were on their respective git repositories and the delta wasn't that much with what was on my laptop. Which is why I would like to use this opportunity to remind we developers again that we should always push to our code repositories as often as we can to a current working branch. Even if the task is not completely done or the code ain't compiling after working on it over time. It doesn't have to be everyday but at least make sure you commit and push within the week. That push could make you gush later in the future.

Now that we have that out of the way, let's focus on what I really want to talk about in this article. In my last two articles here and here, I demonstrated on how to build REST APIs and integrate them with external APIs using Design patterns and best practices in developing applications. In this article I will be demonstrating on how to implement Security using JWT to secure our REST APIs. I will also highlight other strategies in securing our application especially with respect to data. I know this might be familiar topic, but they are usually written in isolation from the overall context of developing application. So we will be building on what we have developed in the previous articles I wrote. I will still be using the same approach in terms of class, component and method referencing so you might want to access the repo here and also take a look at the previous articles to play catch up a bit.

Code Implementation

Imagine you are a consultant, and you are about to book an appointment with a new client of yours. You know that the drill is to first register with the client as a consultant where you provide all your details that will be kept in their records. This is akin to the Registration process that we do online when you want to use an application for the first time. After registration you then proceed to booking an appointment at an agreed upon date. Upon arrival at the premise, you identify yourself at the reception and if all checks out you are then given a visitor's tag or card which grants you access to certain parts or floors of the building. This process could be mapped to the login process of the application using your credentials when you registered. This is the Authentication process and if your credentials are valid, you are granted access to the application. At the same time you are also issued a JWT(token) (or a session is generated for you in a stateful web application or a cookie for the browser). This token represents the visitor's tag that was issued to you. And just like the tag, token will grant you controlled access to resources in the application domain based on the role(s) assigned to the token. This is used in the Authorization process to determine whether you can access a resource or not based on your role(s) (just as how the visitor's tag will only grant you access to certain floors in the building). Now just like the tag which can only be used for that day only, the token also has an expiration date and becomes invalid hence resulting in authorization failures and denial of access to resources. In some cases though, assuming you are not done with your consultation for that day, the tag can be reused for the next day based upon agreement. In the same vein, the token can also be refreshed upon request and a new expiration date is set for it after validating the old token. In the event, you are done with your consultation, you have to submit the tag before leaving the building. Likewise when you are done using the application, you need to logout so that the token is invalidated. And once you are out of the building or logged out of the application, you will have to repeat the identification process at the reception or the login process again to access the facility or the application in order to perform your activities. Now that we have a picture of how Authentication, Authorization and Token Generation and Management work together, Let's connect the dots using the Spring Security Framework and JWT:

First we create our JwtManager class to handle generating, validating and retrieval of information from our token. Then we create our CustomUserDetails class. This class will store details of the Spring authenticated user or prinicipal. By implementing the org.springframework.security.core.userdetails.UserDetails interface, it can access information such as username, password and the authorities granted to the principal via the user's assigned role(s). Next we create our CustomUserDetailsService class. This class defines the custom implementation of the loadUserByUsername method in the org.springframework.security.core.userdetails.UserDetailsService interface that it implements and based on the outcome returns the CustomUserDetails instance we defined earlier. In this case we are querying the database with the username supplied to see if it exists or not.

We then create our JwtAuthenticationFilter class. This is class filters every incoming request against a protected resource to inspect and validate the token that is sent using the validateToken method defined in our JwtManager class. If valid, it extracts the username (subject) and the roles from the token claims using the getUsernameFromJWT and getRolesFromJwt methods respectively that were defined in our JwtManager class and uses it to create a valid secuity context for the user so the user remains authenticated in the application. We could have equally used a second approach with just the username and the loadUserByUsername method of the CustomUserDetailsService class to achieve this with this code snippet

        String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && jwtManager.validateToken(jwt)) {
                String username = jwtManager.getUsernameFromJWT(jwt);

        //Using an injected CustomUserDetailsService instance
        CustomUserDetails customUserDetails = customUserDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(customUserDetails.getPrincipal(), "", customUserDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }else {
                SecurityContextHolder.getContext().setAuthentication(null);
            }

The disadvantage with the second approach is that there will be a performance database hit using the loadUserByUsername method for every request unlike the first approach where we gained application performance by retrieving the user's detail from the sent token. On the other hand, the second approach allows for real time modification of granted authorities when user roles are changed whereas the first approach will require the token to expire before it takes effect. Whichever approach you employ here is usually based on a design and requirement decision.

Next we will create our CustomRestAccessDenied and CustomRestAuthenticationEntryPoint classes. As you can see, they implement the org.springframework.security.web.access.AccessDeniedHandler and the org.springframework.security.web.AuthenticationEntryPoint interfaces respectively. The handle method in the CustomRestAccessDenied class is meant to be triggered for requests where users have been authenticated but do not have the permissible role to access the resources and returns a HTTP 403 status while the commence method in the CustomRestAuthenticationEntryPoint class is triggered for unauthenticated users and returns a HTTP 401 status. You will see how this is setup when we look at our Security Configuration class implementation. Also note how the response is tailored to return in a particular format. This is so that we adhere to a consistent response structure in our application. For those who saw my last article on Exception handling, you maybe wondering why didn't we use that approach in this scenario. The reason for this is that the Access Denied and Unauthorized exceptions are thrown at an outer level than the Spring's ControllerAdvise and Exception can handle hence we have to handle these exceptions at the level which they occur.

Now let's take a look at our Security configuration class which is the SecurityConfig class. The SecurityConfig class which extends org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter is what glues all the previous security components and classes we have defined so far. It determines the behaviour and roles they play in our application security implementation. The securedEnabled, jsr250Enabled and prePostEnabled annotations allows us to determine what roles should have access to a particular resource. This can be configured either on the methods you want to apply these restrictions to (for example in the controllers) or append it to the antMatchers as can be seen in the configure method of the SecurityConfig class. The CustomUserDetailsService class we defined earlier is injected and specified to be used as our authentication component in the overridden public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) method. We also configure a PasswordEncorder bean to use the Spring's BCryptPasswordEncoder to validate the user's password also in this method. In the overridden protected void configure(HttpSecurity httpSecurity) method, we configured the Exception handlers for the unauthorized and access denied scenarios to use the injected CustomRestAuthenticationEntryPoint and CustomRestAccessDenied classes respectively. We disabled the csrf so that requests from other domains can access our endpoints and then we declare the session management to be stateless so as to conform to REST specifications. We then configure our injected JwtAuthenticationFilter class as the filter that will be called for every request to any of our secured resources. And finally we disable our security for endpoints within the /api/auth/ route as this will be called by unauthenticated users during registration and login.

Let's now create the components that will allow users to be able to register and login on our application. First we create our User class entity that will map to our users table in the database. We also create the Role class entity as well that will map to the roles table. We then insert into the roles table the following roles: ROLE_USER and ROLE_ADMIN. Then we create the UserRepository and UserService classes for managing our User entity and the RoleRepository and RoleService classes to manage our Role entity. For integrity, we create a RoleType enum that maps to the roles we just created in our roles table. We then create our AuthController class to handle registration and login. As you can see, the registration endpoint is pretty straightforward. We create a user with the assigned roles and store in the database. For the login endpoint, you supply a registered username and password and if authentication by the Spring Security framework is successful, a valid secuity context for the user is set using the Spring's Authencation object. The generateToken method of the JwtManager then takes in the authentication as a parameter and sets a Claims object's subject with the username and puts the authorities gotten from the Principal which were all gotten from the authentication object as roles into the Claims object. A token is then generated using the constructed Claims object and setting the expiry date, time it was issued and finally signed with our token secret key.

Test Run

All is now set so we can launch our spring application using mvn spring-boot:run and then we try to register by hitting the endpoint using our SignUpRequest dto class structure:

    POST    http://localhost:8080/api/auth/register
    {

    "name":"user1",
    "username":"username1",
    "email":"username1@email.com",
    "password":"password1"
    }

       Response:
    {
        "status": 0,
        "message": "User registered successfully",
        "result": null
    }

Repeat the above for another user to create and admin

    POST    http://localhost:8080/api/auth/register/admin
    {

    "name":"user2",
    "username":"username2",
    "email":"username2@email.com",
    "password":"password2"
    }

        Response:
    {
        "status": 0,
        "message": "Admin registered successfully",
        "result": null
    }

Login with both users using our LoginRequest dto class structure and get the respective tokens that will be returned for both users

    POST http://localhost:8080/api/auth/login

    {
    "username":"username1",
    "password":"password1"
    }

        Response:
    {
        "status": 0,
        "message": "Token generated successfully",
        "result": {
        "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VybmFtZTEiLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTU2MDk4MDU1MywiZXhwIjoxNTYwOTgyMzUzfQ.boqHQh3gLPgNWBP0GiATBGg-25bwMfg33-zn5BFK9AiFuhYcWcmSSFp_isjlhL_xJ9WxMW6A7UWaVOXMdx94ng"
        }
    }

Calling any of our endpoints without a token or an invalid or expired token will give you a response triggered by our CustomRestAuthenticationEntryPoint handler with a HTTP 401 status code

    {
        "timestamp": "2019-06-19T22:51:37.189",
        "message": "Unauthorized user",
        "status": 401
    }

While calling an endpoint with an unauthorized role say

http://localhost:8080/card-scheme/stats?start=1&limit=10

with the user1 generated token will give a response triggered by our CustomRestAccessDenied handler with HTTP 403 status code because only Admin roles can access it.

    {
        "timestamp": "2019-06-19T22:51:57.213",
        "message": "Access Denied",
        "status": 403
    }

Unit Testing

Let's now look at how we will setup writing our tests. Because we are concerned about Authentication and Authorization, we will be restricting our test scopes to not go beyond the controller layer as this is where these restrictions are placed. To achieve this I created the SecurityTest class and annotated it with the @WebMvcTest annotation. Then I mocked every dependencies that are needed at the controller layer except for JwtManager which I spied. The reason being that I will actually be calling the real methods in the JwtManager class in our test for token generation and validation. Then I created AppTestConfig configuration class and set the base package scanning at the controller layer. However I had to create an EntityManagerFactory bean that will be using an in-memory H2 database for startup and bootstrapping. In my setUp method, I then added the JwtAuthenticationFilter instance as a filter for validating the token. And finally I enabled Spring security with .apply(springSecurity()). Running the tests will give us the expected outcomes as defined in our test class.

Before we conclude on this article, let's talk about other aspects of securing our application.

Data Encryption

Data Encryption has been around for as long as the ability for humans to communicate came into being. I will not go into historical accounts of these evidences but data encryption is almost just as important as the exchange of information between individuals or parties. Data Encryption is simply the act of making information useless or cryptic to unintended recipients except for the intended recipient(s).

In today's world, Data Encryption is based on two different forms of cryptography:

  1. Symmetric key
  2. Asymmetric key.

The symmetric key form which uses the AES algorithm, uses a single key both for encryption and decryption and is shared by parties involved. On the other hand, asymmetric key which uses the RSA algorithm involves two keys, public and private keys. The public key can be shared with anyone, but the private key must remain a secret. Both can be used to encrypt a message, and the opposite key from the one originally used to encrypt that message is then used to decode it. Also there is the concept of signing the encrypted data which is very useful for Non-repudiation. This invloves using the private key to sign the encrypted and the public key is then used to verify the signature.

Although there was really no use case to implement any of these encryption forms in our application, you can take a look at the RSAEncryption, AESEncryption and EncryptionAndSignTest classes I created to see the available utility methods and how they are used to perform various operations. Notice how in the RSAEncryption class how you can either generate keys dynamically by calling the generateKeyPair method or load from a keystore file by calling the getKeyPairFromKeyStore method. The keystore file can be generated using the Java keytool utility. One guiding rule in determining which form to use (RSA or AES) is the size of the data to encrypt. For large amounts of data, it is advised to use the AES form as it's faster and less computationally intensive while the RSA can be used for smaller amounts of data.

Data encryption can be done both at rest and in motion.

A common example of data at motion encryption is the use of the HTTPS protocol. What happens here is that when the browser makes a request to a SSL server, it gets the public key and generates a random based key. It then uses the public key to encrypt the generated key and sends it back to the server. The server then uses the private key to decrypt and retrieve the generated key. It is this generated key that both the server and the browser use for encryption and decryption when exchanging information. The advantage of this is that only the browser session and the server knows the generated key used for exchange of information.

For non browser applications, we can simulate the above scenario as well whereby key pairs are generated (public and private keys using the encryption classes I just mentioned above). The involved applications then exchange their public keys and is/are used to encrypt and verify signatures while the private keys are used to decrypt and sign the data.

So in our case here, we can equally achieve data encryption for data in motion by leveraging our application on the HTTPS protocol. Hence our solution will be more of an architectural implementation rather than a coding one.

In the case of data encryption at rest, this usually involves protecting data within our data storage components or systems such as our databases (be it RDBMS, NOSQL or even our caching systems) and even our configuration file values. In other words, using the encryption utility classes I created earlier, we can store our data in an encrypted format and only decrypt when it is about to be used within the application. This approach will make the data in our datastore cryptic and unreadable should it fall into the wrong hands. We can also choose to encrypt our data without expecting to decrypt it back so long as we know the algorithm we used in encrypting it in the first place. This is a lot safer than encryption and it is known as Hashing.

A classic example of hashing here is the storage of users' passwords in our database (Recently there was an outcry of Facebook storing passwords of Instagram users in plain text). We achieved it here using the hashing implementation which is unidirectional. In otherwords unlike the encryption technique discussed earlier, it can't be decrypted (or impossible in practicality) after it's been hashed. The principle here is that hashing a particular sequence of string with a particular algorithm will always produce the same encrypted string (except in the case of Salting which I will discuss shortly). Here we used the BCryptPasswordEncoder encode method to achieve this as can be seen in our SecurityConfig class where it is being used as our PasswordEncoder and used when creating our users during the registration process.

Merely hashing our passwords isn't just enough to secure them. The availability of Rainbow tables has proven this to be so. The online availability of these tools have made cracking passwords a hobby for hackers especially the very simple and common ones such as popular words, sequential alphabets and numbers, and even personal information such as names, dates etc. This has led to the concept known as Salting. Salting is a technique whereby random sequence of characters are added as part of the parameters required for password hashing. It makes using rainbow tables very difficult to use as even the simple password instances mentioned above will no longer have entries in these tables as a result of salting.

In older implementations, Spring provided the MessageDigestPasswordEncoder class which allowed us to provide our own salt string for hashing our password. An efficient usage of this was to provide a different salt string for every password string to be hashed (e.g time in milliseconds at point of registration). However this has been deprecated in favour of the BCryptPasswordEncoder which we are currently using in our application. The BCryptPasswordEncoder not only hashes our password but internally generates a random salt string for us in the process as can be seen when looked at its internal implementation. This implementation unburdens us with the overhead of managing the generation of salt strings as this crucial in the integrity of our passwords. As a result of this, a different hashed string produced everytime you hash the same password string. Now the question arises how do we determine compare and match the passwords? The BCryptPasswordEncoder implementation breaks down the hashed string into the salt string, cost parameter and the hashed value all of which can be obtained from the hashed string and since the salt determines the generated hash, it can be used to determine whether the supplied passwords match or not with what we have in store.

And finally, I'd like to talk about securing our application configuration file which is usually either named as application.properties file or application.yaml as in our case. Although in our case you will notice that sensitive information like the database credentials are currently being retrieved from environment variables, I've seen cases where these details are stored as plain text within the file. Even though the use of environment variables mitigates to an extent the risk of having them as plain text in the file, another approach will be the use of Jasypt Spring boot starter module. This module allows us to store values in its encrypted form and then decrypted for use at runtime.

To implement this in our application, we first need to add the jasypt-spring-boot-starter to our pom.xml

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

Then we run the command below

java -cp $M2_REPO/org/jasypt/jasypt/1.9.2/jasypt-1.9.2.jar  org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="yourpassword" password=jasypt_password algorithm=PBEWithMD5AndDES

where

  • $M2_REPO is your maven repoistory directory
  • input argument is the password u need to encrypt say for example your database password
  • password argument is the Jasypt password or key for encryption you will be supplying when starting up your application
  • algorithm is the Password Based Encryption (PBE) algorithm of your choice provided by Jasypt (PBEWITHMD5ANDDES, PBEWITHMD5ANDTRIPLEDES, PBEWITHSHA1ANDDESEDE, PBEWITHSHA1ANDRC2_40)

The above command will produce an encrypted value of the input argument password value you provided. You can repeat the command for any other values you may wish to encrypt in the application.properties or yaml file.

You then set the properties or environment variable value(s) with these encrypted value in this format ENC(encrypted_value) for example in our yaml you can change the password value like this

.
.
.
spring:
  profiles: development
  application:
    name: binList
  datasource:
     password: ENC(encrypted_password_from_jasypt_operation)
     .
     .
     .

or you simply leave the yaml file as is and set the SPRING_DATASOURCE_PASSWORD environment variable with it

export SPRING_DATASOURCE_PASSWORD=ENC(encrypted_password_from_jasypt_operation)

Then you either start your application like this

mvn -Djasypt.encryptor.password=jasypt_password spring-boot:run

or you export the JASYPT_ENCRYPTOR_PASSWORD environment variable and simply just run mvn spring-boot:run

export JASYPT_ENCRYPTOR_PASSWORD=jasypt_password

To further tighten security on our application server, you can do the following steps sequentially

  1. Export and set all the environment variables mentioned earlier using a batch script (.sh or .bat depending on your OS)
  2. Start up the application as a background service like this mvn spring-boot:run & or as a Windows Service
  3. unset all the environment variables using a batch script since these values are only requested for once on application start up.
  4. Delete the batch script(s) so long as you have a copy elsewhere.

The reason for these steps is to prevent users querying your application server using the ps and history commands to view the passwords or environment variables from previously ran commands.

So there you have it. We have secured our application although as we all know security can never be total but it should slow down and require more effort for someone to maliciously use our application. There are other security strategies I have left out here especially at Data Access Object layer such as query input parameterization for SQL injection due to lack of SQL or JQL implementations in our application. You can access the updated source code for this article here.

In my next article if time permits me I will be demonstrating how to achieve Scalability and Availability with our application using Docker and some code refactorings. Also I hope to recover the Laravel and Node(NestJs) implementations of what we have done so far from my laptop crash and share it. That's all folks! Happy Coding.

Top comments (0)

🌚 Life is too short to browse without dark mode