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:
- Login via API: We'll kick things off by tackling the initial API login.
- Implementing a JWT for Persistence: Learn how to keep your users authenticated via API using the shiny new JWT.
- 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>
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)
}
}
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.
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()
}
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());
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)
}
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)
}
}
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 ofAuthenticationProvider
. This configures ourAuthenticationManager
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:
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)
}
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()
}
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()
}
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. ๐
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.
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)