DEV Community

Ruben Suet
Ruben Suet

Posted on

Unleashing the Power of Spring Boot and Kotlin: Modernizing API Authorization Part II

In our previous discussion, we explored the interns of Spring Security, gaining insights into the orchestration of filters and the crucial function of the AuthenticationFilter in user authentication. Now, we switch our attention to a practical skill: the generation of Json web tokens. This skill will enable us to tap into the advantages of token-based authentication, enabling a process of user authentication smooth when it requests the different API endpoints from our app.

This article is your guide to mastering three key aspects in three parts:

  1. Login via API: We'll kick things off by tackling the initial API login.
  2. Implementing a JWT for Persistence: Learn how to keep your users authenticated via API using the shiny new JWT.
  3. Connecting Database Users to Spring Security: Dive into the realm of syncing your database users seamlessly with the Spring Security system.

You might have observed that even after a successful login, the user is restricted from performing any subsequent actions. This limitation arises because we've configured all endpoints to require authentication, and with each HTTP request, the process starts anew, lacking persistence for the user. To address this, JSON web tokens(JWT) can be a valuable solution, allowing us to maintain user sessions for continued operations.

Our approach involves issuing a token with a validity period of up to 10 days. Beyond this duration, attempting further operations will result in a forbidden response, prompting the user to undergo the login process once again. To implement this, let's include the necessary dependencies in our pom.xml.

pom.xml

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.12.3</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.12.3</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.12.3</version>
  <scope>runtime</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

In contrast to the Spring Security package, the dependencies include a specified version. The version mentioned here corresponds to the one in use at the time of making this article. However, feel free to check for the latest versions on the Maven Repository website and opt for the most up-to-date version.

Json Web Token diagram

Esuring secure encryption, a JWT incorporates a signature that involves both the encryption algorithm and a secret key influencing the encoded token's outcome. The process involves three crucial steps:

  • JWT Creation upon User Login: Generate a JWT when a user successfully logs in.

  • JWT Verification on Each Endpoint Request: Validate the JWT on every endpoint request.

  • Token Validation with Correct Signature and Expiry Time: Validate the token by ensuring it has the correct signature and has not expired.

Create a JWT when a user is logged in

Let's initiate the process by creating a new file within the security package:

JwtGenerator.kt

@Component
class JwtGenerator() {

    fun makeToken(username: String): String {
        val secretWord = "This is my  secret word for Dev.to"
        val keySecretWord = Keys.hmacShaKeyFor(secretWord.toByteArray())

        return Jwts.builder()
            .subject(username)
            .issuedAt(Date())
            .expiration(Date(System.currentTimeMillis() + 864000000)) // Token for 10 days
            .signWith(keySecretWord)
            .compact()
    }
}
Enter fullscreen mode Exit fullscreen mode

We designate this class within the Spring Boot context using the @Component annotation since it lacks Business Logic. While an @Service alternative is plausible, the choice is flexible based on your preference. Although the use of an object instead of a class is plausible, a future refactor will introduce the injection of the keySecretWord variable rather than hard-coding it here.

The line val keySecretWord = Keys.hmacShaKeyFor(secretWord.toByteArray()) is crucial for creating a SecretKey object. Here, the HMAC-SHA algorithm is employed, as detailed in the documentation. Should a different algorithm be more suitable for your needs, feel free to adjust it accordingly. The essential aspect is ensuring the presence of a SecretKey type.

It's important to note that the method Keys.hmacShaKeyFor(secretWord.toByteArray()) requires a ByteArray type as a parameter, necessitating the transformation from a String.

Transitioning to the second part of the code, its responsibility lies in generating a new JWT:

  return Jwts.builder()
            .subject(username)
            .issuedAt(Date())
            .expiration(Date(System.currentTimeMillis() + 864000000)) // Token for 10 days
            .signWith(keySecretWord)
            .compact()
Enter fullscreen mode Exit fullscreen mode
  • The Jwts.builder() starts the creation of a new JWT based on the information specified in the subsequent commands.
  • The subject command defines the payload content; in this instance, we set the subject as the username, a parameter passed when calling the method.
  • The issuedAt command generates a timestamp indicating when the token is generated.
  • The expiration command determines when the generated token will expire. In our scenario, we set it to +10 days, but you can choose any time duration in milliseconds.
  • The signWith command configures a signature using our encrypted secret word, enhancing the security of the JWT.
  • The compact statement serves as the final directive to the builder, telling our builder that it's time to assemble the instance.

With this file, we can now integrate it into our login controller.

LoginController.kt

@RestController
class LoginController(
val authenticationManager: AuthenticationManager,
val jwtGenerator: JwtGenerator) {

  @PostMapping("/login")
  fun login(@RequestBody loginRequest: LoginRequest): ResponseEntity<Void> {

     val authenticationRequest = UsernamePasswordAuthenticationToken.unauthenticated(                
       loginRequest.username, 
       loginRequest.password
     )

     val authenticationResponse = authenticationManager.authenticate(authenticationRequest)

     val token = jwtGenerator.makeToken(loginRequest.username)

     return ResponseEntity.ok(LoginResponse(token))

  }

  data class LoginRequest(val username: String, val password: String)
  data class LoginResponse(val token: String)

}
Enter fullscreen mode Exit fullscreen mode
  1. The constructor injects the JwtGenerator.
  2. A token is generated using jwtGenerator.makeToken(loginRequest.username) and stored in a variable.
  3. If the token creation is successful, it returns the LoginResponse object.
  4. The LoginResponse is a data class (data class LoginResponse(val token: String)) designed for returning data in JSON format. The response contains the generated token.

This is the outcome of the login process now:

Thunderclient login response with JWT

Verify JWT

To make use of the received JWT, we need a mechanism to inform Spring Security that the token is verified. If the verification is successful, the user can then access the requested resource. Let's implement a token filter within the security package:

JwtTokenFilter.kt

@Component
class JwtTokenFilter() : OncePerRequestFilter() {


    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
      val secretWord = "This is my  secret word for Dev.to"
      val keySecretWord = Keys.hmacShaKeyFor(secretWord.toByteArray())
      try {
        extractToken(request.getHeader("Authorization"))?.let { token ->
           Jwts.parser()
           .verifyWith(keySecretWord)
           .build()
           .parseSignedClaims(token)
           .payload
           .subject
           .let { user ->
             SecurityContextHolder.getContext().authentication =
                            UsernamePasswordAuthenticationToken(user, null, emptyList())
            }
          }
        } catch (e: SignatureException) {
            SecurityContextHolder.clearContext()
        }
        filterChain.doFilter(request, response)
    }

    private fun extractToken(authorizationHeaders: String?): String? {
        return authorizationHeaders?.takeIf { it.startsWith("Bearer ") }?.substring(7)
    }
Enter fullscreen mode Exit fullscreen mode

This class inherits from OncePerRequestFilter, a subclass of GenericFilterBean. Understanding the difference between these two is crucial:

  • GenericFilterBean is a special class where you must implement the doFilterInternal function. The logic you put inside will be encapsulated and run arbitrarily through Spring Security filters. Spring Security determines when this logic runs in the filter chain but can apply it multiple times.

  • OncePerRequestFilter is similar to the above class but with a crucial distinction: the logic runs only once. It forces Spring Security to confirm that if the logic is validated, there is no need to revisit and re-check it.

The implementation convention for the doFilter method is as follows:

override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
Enter fullscreen mode Exit fullscreen mode

Making sure that you include the parameters in this method is vital. At the end of our method, we execute filterChain.doFilter(request, response). This statement ensures that the filters from the filter chain continue to run. Forgetting this line may break the flow and result in unexpected behavior.

Regarding Keys.hmacShaKeyFor, it is not necessary to provide an additional explanation. Just remember that it must exactly match the value used as a String previously. We will refactor it later to centralize this information, but for now, we'll proceed with a small proof of concept and make adjustments afterwards.

 extractToken(request.getHeader("Authorization"))?.let { token ->
           Jwts.parser()
           .verifyWith(keySecretWord)
           .build()
           .parseSignedClaims(token)
           .payload
           .subject
           .let { user ->
             SecurityContextHolder.getContext().authentication =
                            UsernamePasswordAuthenticationToken(user, null, emptyList())
            }
          }
        } catch (e: SignatureException) {
            SecurityContextHolder.clearContext()
        }
        filterChain.doFilter(request, response)
    }
Enter fullscreen mode Exit fullscreen mode
  • We extract the token based on the helper method mentioned at the end of the file. This small helper method takes the first 7 characters from the authentication in case it exists. Typically, for JWT authentication, a common convention is to add the prefix "Bearer" and then the token. You can use any prefix, but it's crucial to consider the character count for extracting the prefix and white space. In ThunderClient for VSCode, there's an option for "Bearer" token type authentication, allowing easy inclusion of the prefix.

Thunderclient with Bearer token

If, for some reason, your API client lacks an authorization section, you can manually add it to the headers.

API client with header Authorization

  • Once the token is extracted from "Bearer," we pass the value into a new context named token using the let command. The ? before ensures that we have a value or return null otherwise, preventing the execution of the following block context if no value is present.
  • The JWT parser is invoked to transform the encrypted JWT into a proper payload for reading. The same secretKeyword signature is used to apply the algorithm. It's crucial to use the same string; otherwise, the process will fail.
  • The token is built, and its signature is verified with parsedSignalClaims, where it checks the algorithm and secret word inside the token against our own signature (HMAC-SHA algorithm and the secret word).
  • We extract the payload (the content of the returned token, where the username was placed in the subject during token generation). The subject is extracted, holding the username.
  • In the following critical process, if we have a username, a new context with let is invoked to pass the username as a value. SecurityContextHolder is invoked manually. Remember, the SecurityContext holds all the critical information. We're manually stating that this token corresponds to an authenticated user. We save the authentication as a new UsernamePasswordAuthenticationToken object, holding our username. The three parameters passed are:

    • Principal, which holds the username.
    • Credentials, holding the password. As we do not store a password in our JWT, it's set as null.
    • Authorities, representing the roles of the user. This is related to the authorization topic, which we do not address here. If needed, you could store the user's role in the payload and set it here. In our case, we pass an empty list because we treat all users on the website as the same type.

With this file, we can filter and check the provided token from the user, ensuring its validity based on our JWT signature, and set our user as authenticated. One part is missing: ensuring that our JWT filter is applied before the authentication filters like BasicAuthenticationFilter or UsernameAndPasswordFilter. Let's update our security filter chain method to instruct the framework to apply our custom filter before the authentication filters.

SecurityConfig.kt

@Configuration
class SecurityConfig {
  // Other methods....
   @Bean
   fun securityFilterChain(http: HttpSecurity, jwtTokenFilter: JwtTokenFilter): SecurityFilterChain {
      http {
        csrf { disable() }
        addFilterBefore<UsernamePasswordAuthenticationFilter>(jwtTokenFilter)
        authorizeHttpRequests {
           authorize("/login", permitAll)
           authorize(anyRequest, authenticated)

        }
      }
        return http.build()
  }
}
Enter fullscreen mode Exit fullscreen mode

The JwtTokenFilter is injected, and the method addFilterBefore is specified. In our language, addFilterBefore<UsernamePasswordAuthenticationFilter>(jwtTokenFilter) indicates: Before the UserNamePasswordAuthenticationFilter, please apply the custom jwtTokenFilter. Now our filter placement is not arbitrary; we have deliberately decided where to position it. This ensures that an authenticated user is created before all the necessary checks are applied, facilitating the framework's operation and allowing us access to the endpoints.

Thunderclient success login with JWT step1

Thunderclient failed authorization without JWT step2

Thunderclient successful authorization with JWT provided step2

Validate JWT

This is already looking good, but currently, only the signature is being checked. What if we also want to verify the expiration date? Same as before, the validation logic will be implemented in another file in the security package, where we set the same secret word and algorithm to transform it into a SecretKey and then validate.

JwtTokenValidator.kt

class JwtTokenValidator {
    fun validateToken(token: String) {
      val secretWord = "This is my  secret word for Dev.to"
      val keySecretWord = Keys.hmacShaKeyFor(secretWord.toByteArray())
        try {
          val jws: Jws<Claims> = Jwts.parser()
            .verifyWith(keySecretWord)  
            .build()
            .parseSignedClaims(token)      

          if (jws.payload.expiration.before(Date())) {
              throw RuntimeException("Token is expired")
          }

        } catch (e: SignatureException) {
            throw RuntimeException("Invalid Token Signature", e)
        } catch (e: Exception) {
            throw RuntimeException("Token Validation Failed", e)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, I've chosen not to annotate it with @Component since I will only use it in the JwtTokenFilter, and I already have all the necessary information to make it work. Therefore, the Spring context is not needed to manage this file. Focusing on this snippet:

 val jws: Jws<Claims> = Jwts.parser()
            .verifyWith(keySecretWord)  
            .build()
            .parseSignedClaims(token)      

          if (jws.payload.expiration.before(Date())) {
              throw RuntimeException("Token is expired")
          }
Enter fullscreen mode Exit fullscreen mode

As in the filter, it is parsed from what is received as a token into an object that can be processed. In this case, we check again the signature with verifyWith and parseSignedClaims. In reality, this step could be skipped since we are already checking it before in the filter, but I think it does not harm to check it out twice, and it's not computationally expensive if checked twice.

The interesting part is when we are extracting the expiration, and it is compared with a Date() that will return your current system time. If it's expired, then an exception is thrown. We can test this validation if we change the expiring date from our JWT when it's generated.

Let's add the validation file to our filter:

JwtTokenFilter.kt

class JwtTokenFilter() : OncePerRequestFilter() {

  private val tokenValidator = JwtTokenValidator()

 override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        // Secret words variables...
        try {          
          extractToken(request.getHeader("Authorization"))?.let { token ->
                tokenValidator.validateToken(token)
                Jwts.parser()
                    .verifyWith(keySecretWord)
                // rest of the method...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice how I added tokenValidator as a property in the class, and then when the token is checked via let, before proceeding, we apply our validateToken by sending the token.

You can change your JWT generator milliseconds option to only last for 10 seconds; try to log in and check an endpoint constantly with the token for the following 10 seconds, and you will notice that eventually, it will return a 403 forbidden.

Refactoring the secret word

It is not ideal to have the same secret word defined and transformed into SecretKey up to three times. Let's add the word in our application.properties.

application.properties

jwt.secret-word=This is my  secret word for Dev.to
Enter fullscreen mode Exit fullscreen mode

In our SecurityConfig file, we can retrieve it as a @Value and create a @Bean for it.

SecurityConfig.kt

@Configuration
class SecurityConfig {
    @Value("\${jwt.secret-word}")
    val secretWord: String? = null

    @Bean
    fun jwtSecretKeyword(): SecretKey = Keys.hmacShaKeyFor(secretWord?.toByteArray())

  // The other methods that we worked through this tutorial
}
Enter fullscreen mode Exit fullscreen mode

@Value injects into secretWord the value of our application.properties. Since this is Kotlin, the $ has a special meaning, and we need to escape it to make it work for the value. Now it's only a matter of injecting it into the constructor of the generator and the filter:

JwtGenerator.kt

@Component
class JwtGenerator(private val jwtSecretKeyword: SecretKey) {
  fun makeToken(username: String): String {
     // I removed the secretWord 
     return Jwts.builder()
     // Methods from the builder
      .signWith(jwtSecretKeyword) // injected secret word
     // Rest of the method
  }
}
Enter fullscreen mode Exit fullscreen mode

JwtTokenFilter.kt

@Component
class JwtTokenFilter(private val jwtSecretKeyword: SecretKey) : OncePerRequestFilter() {

    private val tokenValidator = JwtTokenValidator(jwtSecretKeyword) // I am sending the secret word from the constructor to the validator

  // change .verifyWith(keySecretWord) with .verifyWith(jwtSecretKeyword)
Enter fullscreen mode Exit fullscreen mode

In our validator too:

JwtTokenValidator.kt

class JwtTokenValidator(private val jwtSecretKeyword: SecretKey) {
   // change .verifyWith(keySecretWord) with .verifyWith(jwtSecretKeyword)

}

Enter fullscreen mode Exit fullscreen mode

This is why I wanted to go with a class since the beginning with this file. If we didn't have anything in the constructor, we could have an object instead of a class, which would have ensured that this would be a singleton in our app. However, since this will only be used in the TokenFilter and it will be already a bean singleton, our validator should be safe as well.

One last refactoring to apply: Our Controller is managing the login system from the previous section. We could move the authentication process to a service, and then our controller would only be able to receive the HTTP call and send the info to the different services that will manage the logic.

Let's create an AuthenticatorService in our security package:

AuthenticatorService.kt

@Service
class AuthenticatorService(val authenticationManager: AuthenticationManager) {
    fun authenticate(username: String, password: String) {
        val authenticationRequest =
            UsernamePasswordAuthenticationToken.unauthenticated(
                username, password
            )

        authenticationManager.authenticate(authenticationRequest)
    }
}

Enter fullscreen mode Exit fullscreen mode

And our LoginController will look like this:

LoginController.kt

@RestController
class LoginController(
    val jwtGenerator: JwtGenerator,
    val authenticatorService: AuthenticatorService,
) {

    @PostMapping("/login")
    fun login(@RequestBody loginRequest: LoginRequest): ResponseEntity<LoginResponse> {
        authenticatorService.authenticate(loginRequest.username, loginRequest.password)

        val token = jwtGenerator.makeToken(loginRequest.username)

        return ResponseEntity.ok(LoginResponse(token))
    }
}
Enter fullscreen mode Exit fullscreen mode

In this section, we learned:

  • Creating our own JWT when a user logs in
  • Know about how to add custom filters in the FilterChain and verify from there our JWT
  • Place our custom filter in a specific order
  • Validate our token to make sure that the signature and expiry date are still valid

Top comments (0)