DEV Community

Ruben Suet
Ruben Suet

Posted on

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

So, you've decided to dive into the world of API authorization with Spring Boot and Kotlin. Exciting, right? Well, almost. As I went deeper into the docs and various tutorials, I stumbled upon a common frustration - outdated syntax everywhere!

Picture this: Most JWT methods are stuck in the past, and even some parts of the Spring Security package haven't received the memo on updates. Talk about a coding headache! Thankfully, a bit of Stack Overflow magic guided me to the elusive new syntax.

Now, why am I here writing this? Simple. I want to spare you the frustration and showcase the latest syntax. But hold on, it's not just about the code. I'll walk you through the flow, the gears behind the implementation, and the magic that makes it all tick.

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.

Buckle up! We're about to make your Spring Boot and Kotlin journey smoother and more enjoyable. ๐Ÿš€

Prerequisites

Before delving into the technical intricacies, let's establish some groundwork. This post assumes you've already set up a functioning Spring Boot application intricately tied to your database. If you're not there yet, don't fret. Check out this official Spring Docs tutorial for a comprehensive walkthrough on creating an application and establishing a connection with your preferred database. No need to stress about a User model at this point; we'll revisit that later.

In this discussion, I'll be working with Kotlin 1.9.20, Spring Boot 3.15, and Spring Security 6.15. My project aligns with Maven, but fear not, Gradle aficionados โ€“ adapting the steps to your build script is a breeze.

Now, let's dive into the essentials with a touch of seriousness.

Login via API

Your application is live, the database is connected, and your API endpoints are ready for action. Now, let's integrate Spring Security into your pom.xml.

pom.xml

<dependency>             
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Upon starting your server and attempting to access an endpoint, a 403 forbidden response may halt your progress. This occurs because Spring Security defaults to requiring user authentication for all pages, restricting access to resources. Remember, Spring Security excels in both Authentication and Authorization, and today, we're focusing on the authentication aspect.

To enhance security, let's create a new package named security within your app. Most of our code will find its home there. Now, proceed to craft a new configuration file inside this dedicated package.

SecurityConfig.xml

@Configuration
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            formLogin { }
            httpBasic { }
        }

        return http.build()
    }

    @Bean
    fun userDetailsService(): UserDetailsService {
        val user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build()

        return InMemoryUserDetailsManager(user)
    }

}
Enter fullscreen mode Exit fullscreen mode

The code, sourced directly from the Spring security docs., might seem complex at first. Let's break it down:

We're establishing a @Configuration class with its own set of @Beans. If these concepts seem unfamiliar, refer to Gustavo Peiretti's detailed article for a clearer understanding.

Now, the crux of the matterโ€” the securityFilterChain method. Spring Security operates on filters, each responsible for specific authentication and authorization tasks.

Filter chain diagram

Remember, these filters manage authentication and authorization. We'll delve into some of them for the authorization process later on. A filter chain is distinct; by creating a method returning a SecurityFilterChain type, you open the door to customizing behaviors for these filters. The filter chain cleverly delegates customization, but maintaining order aligns with the real filters for optimal configuration.

@Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            formLogin { }
            httpBasic { }
        }

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

Having HttpSecurity in our grasp from the methods, we can start shaping our customizations.

Our first move is to employ authorizeHttpRequests. As the name implies, it's all about deciding what's available to both authenticated and non-authenticated users in our app. Notice that for anyRequest, we're declaring that we want the user to be authenticated.

The next player in the scene is formLogin. It signals our intention to use the default login form from Spring Security. However, hold your horses๐Ÿดโ€”we'll remove this later since our goal is API-based authentication. On the other hand, httpBasic clarifies the authentication methods in play. It could be Form, Basic, or Digest. In a nutshell: Form is for HTML form information, Basic is for HTTP authentication, and Digest is an older method, not recommended anymore. Learn more here

Now, let's wire it all together and build our customized rules. Once processed, every rule will seamlessly attach to its designated parts in the different filters and work its magic.

If you're curious about where these rules go, here's a sneak peek for our small example:

1.The authorizeHttpRequests configuration governs access control and authorization. It typically aligns with the FilterSecurityInterceptor in the filter chain, enforcing access control rules.

2.The formLogin configuration is all about form-based authentication. It corresponds to filters like UsernamePasswordAuthenticationFilter and others in the authentication section of the filter chain. This filter handles the processing of login forms.

3.The httpBasic configuration is linked to HTTP Basic authentication. It usually syncs up with the BasicAuthenticationFilter in the filter chain, dealing with basic authentication requests.

An important note: Spring Security offers its DSL for Kotlin, enabling the bracketed syntax for each command. In Java, this would be something like:

http
  .authorizeHttpRequests((authorize) -> authorize
    .anyRequest().authenticated()
  )
  .httpBasic(Customizer.withDefaults())          
  .formLogin(Customizer.withDefaults());
Enter fullscreen mode Exit fullscreen mode

Even though this is not related to Spring security, Learn more about Kotlin DSL here

Moving on to the next piece of config:

@Bean
  fun userDetailsService(): UserDetailsService {
    val user = User.withDefaultPasswordEncoder()
    .username("user")
    .password("password")
    .roles("USER")
    .build()

  return InMemoryUserDetailsManager(user)
}
Enter fullscreen mode Exit fullscreen mode

This segment is a critical aspect of our app. Spring Security always verifies users and cross-checks their information through UserDetails. While we plan to implement our custom UserDetails and UserDetailsService down the road, let's first establish a solid proof of concept.

In this method, we're creating a new user with a username, an encoded password, and a role. This information is then used to construct the user, and we return an InMemoryUserDetailsManager. This manager crafts a user in memory for the app, allowing us to concentrate on refining our login system without being bogged down by database intricacies.

Later on, we'll seamlessly integrate our users from the database into the framework. But for now, this approach provides a pragmatic solution for our initial focus on the login system. ๐Ÿš€


With our user created and the basic configuration in place, let's now enable a REST endpoint for the login process. We'll augment our config file with two additional methods:

@Configuration
class SecurityConfig {
  // Our user details and security filter chain methods
 @Bean
   funpasswordEncoder() : PasswordEncoder = BCryptPasswordEncoder()

@Bean
  fun authenticationManager(
    userDetailsService: UserDetailsService,
    passwordEncoder: PasswordEncoder): AuthenticationManager 
 {
    val authenticationProvider = DaoAuthenticationProvider() 

    authenticationProvider.
      setUserDetailsService(userDetailsService)      
    authenticationProvider.
      setPasswordEncoder(passwordEncoder)

    return ProviderManager(authenticationProvider)
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break it down:

passwordEncoder()
Starting with the straightforward one: BCryptPasswordEncoder. This is one of the many options available for encrypting and decrypting passwords. By making it available as a bean, we ensure its accessibility across the platform.

authenticationManager()
Now, the AuthenticationManager interface takes center stage. It's where the real magic happens in our app. This interface is responsible for authenticating users, usually taking an Authentication object to determine whether the user attempting to access our app can log in. Upon successful login, it returns an Authentication object specifying the user's authority (or role).

Here's a closer look at what's going on:

  • We create a DaoAuthenticationProvider, an implementation of AuthenticationProvider. This configures our AuthenticationManager to understand how to process information and decide whether to grant authentication to the user attempting to log in.

  • We pass our custom UserDetailsService, complete with our in-memory user, and the encryption system we just created using BCrypt. To dive deeper into the behind-the-scenes mechanics, let's explore this visual aid:

Authentication filter in steps

This illustration sheds light on the intricate authentication process orchestrated by the AuthenticationManager. ๐Ÿ›ก๏ธ Nevertheless, let's break it down:

1.AuthenticationFilter Entry (Filter in the Chain): The user's authentication endeavor begins as they step into the AuthenticationFilter, one of the filters in the chain.

2.Token Generation (UsernamePasswordAuthenticationToken): A token is crafted at this stage. This token, known as UsernamePasswordAuthenticationToken, is initially unauthenticated. It serves as a precursor, guiding the AuthenticationManager on how to handle the incoming request.

3.AuthenticationManager Delegation: The AuthenticationManager takes center stage, determining the user's authentication status. This responsibility is delegated to the ProviderManager, which holds key information:

  • Provider: Specifies from where to fetch user information (UserDetails).
  • Password Encoder: Defines the encryption system for passwords.

4.DaoAuthenticationProvider Activation: The ProviderManager activates the DaoAuthenticationProvider, a critical cog in the authentication wheel.

5.Encryption System Configuration: The selected encryption system for passwords is set up.

6.UserDetailsService Interaction: The UserDetailsService, yet to be fully implemented, will retrieve information about users. Currently, our approach involves in-memory users.

7.UserDetails Lookup: The service seeks insights within UserDetails (to be implemented later), a Spring Security mechanism for managing user-related information.

8.User Model Creation: A preliminary user model is constructed, bridging the gap between our in-memory user configuration and the envisioned fully-fledged entity that will connect with our database (our Entity is yet to be implemented).

9.Backtracking Steps: The journey retraces its steps, reiterating through the authentication mechanisms.

10.Security Context Update: This crucial step updates the Security Context, the repository where all vital information from the filters is stored. It acts as the backbone of Spring Security, holding critical details about the user's authentication status.

Conclusion: The authentication process concludes, leaving the Security Context enriched with user-specific details, confirming their authenticated status. This context becomes the key concept of Spring Security, ensuring the seamless flow of authenticated interactions. ๐Ÿ”„๐Ÿ”

Could you have ever imagined the amount of information embedded in this small @Configuration example? I certainly didn't. If you find yourself a bit disoriented, I suggest revisiting the presented diagram above with the provided example. Understanding the dynamics of the FilterChain and grasping the role of the AuthenticationFilter provides a clear idea of what lies ahead. Take your time to understand it and revisit it, if necessary.

Now, it's time to introduce an endpoint for the Login feature. In your app, you know the prime location for a new controller. For the purposes of this article, I'll place most components within the existing security package to keep them organized. ๐Ÿ—บ๏ธ

LoginController.kt

@RestController
class LoginController(val authenticationManager: AuthenticationManager) {

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

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

     val authenticationResponse = authenticationManager.authenticate(authenticationRequest)

  }

  data class LoginRequest(val username: String, val password: String)
}
Enter fullscreen mode Exit fullscreen mode

In this implementation, we introduce a simple DTO, LoginRequest, which encapsulates the username and password.

The login method begins by crafting the authentication token (authenticationRequest) of type UsernamePasswordAuthenticationToken. This token is then used to authenticate the user through the AuthenticationManager set up in the configuration file. As Spring Security navigates through the various filter chains, it addresses fundamental questions:

  • Does the user exist?
  • Are the credentials accurate?
  • Is the user permitted to access the targeted endpoint? (Authorization)
  • What roles does the authenticated user possess?

These inquiries represent just a fraction of the complexities that Spring Security adeptly manages on our behalf. At the culmination of this chain, armed with a wealth of information, the framework determines whether to authenticate the user and grant access to the endpoint. For our specific login scenario, where authorities are not pivotal, the outcome is binary:

  • Success: The application recognizes you as an authenticated user, resulting in a 200 OK response.
  • Failure: The application denies your login attempt, responding with a 403 Forbidden.

This is the elegance of the AuthenticationFilter ๐Ÿ™‚, effortlessly orchestrating a multitude of tasks. While our SecurityFilterchain primarily concerns itself with authorization, the synergy between these components ensures a robust security framework.

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
                  authorize(anyRequest, authenticated)
        }
        formLogin { }
        httpBasic { }
        }

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

We are checking for every single endpoint and prooving that we are authenticated, including our /login endpoint. Let's modify this snippet that was a literal copy-paste from the Spring docs and adapt it a bit:

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
    http {
            csrf { disable() }
        authorizeHttpRequests {
                authorize("/login", permitAll)
                authorize(anyRequest, authenticated)
        }
        }

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

In this refined configuration, we've removed the unnecessary formLogin and httpBasic configurations since our application exclusively provides REST endpoints, eliminating the need for HTML-based authentication. The csrf protection has been disabled, given the absence of HTML forms. While this decision should be approached cautiously, it aligns with the specific context of not providing HTML in this scenario. It's crucial to address CSRF concerns appropriately, and I strongly advise collaboration with the front-end team for a comprehensive solution.

The addition of authorize("/login", permitAll) ensures an open door for unauthenticated access to the login endpoint, catering to the specific requirements of a RESTful authentication flow. Always keep in mind that the order of authorizations matters, and the app processes them from top to bottom. The final rule, in this case, ensures that any unspecified request must be authenticated. Once you have run the app and sent a request with a username and password, you should receive a delightful 200 OK. ๐Ÿš€

Login response in thunderclient

In this section, we learned about:

  • What is Spring Security and what does it do
  • What are Filters and what is the FilterChain in a Spring Security app
  • How to add our custom logic inside the FilterChainSecurity
  • How to add a user in memory viva UserDetails
  • How the AuthenticationFilter works, and how it works along with AuthenticationManager
  • The importance of Spring Security Context

If you're curious about the inner workings of Spring Security, I'll share a visual representation of the entire framework in action. Keep in mind that you don't need to familiarize yourself with every single filter and implementation. However, grasping the overall functioning of filters, especially in the context of our exploration with the AuthenticationFilter, provides valuable insights. This understanding guides you in navigating the filter chain and making targeted customizations for enhanced security.

Security Filter Chain diagram

Now, imagine the beauty of this illustration ๐ŸŒน. The chain unfolds in a structured manner from top to bottom, showcasing the sequential delegation of implementations. This visual representation highlights the two primary domains that Spring Security effectively handles: Authorization and Authentication. It offers a comprehensive view that helps in understanding how filters are orchestrated and empowers you to make customizations across various segments of the filter chain.

Top comments (0)