DEV Community

Ruben Suet
Ruben Suet

Posted on

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

In the previous articles, I discussed the implementation of a login system in a Spring Boot app using Spring Security and demonstrated how to implement JWT for authenticating users and granting access to specific endpoints. In this final article, we'll explore how to eliminate the InMemoryUserDetailsManager and transition to our custom User model, integrated with our database.

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.

Integrating Database Users with Spring Security

Currently, our application hosts a pre-established user with a designated username and password. While this setup is perfectly suitable for testing or small-scale proof-of-concept applications, in many instances, we want to take a more comprehensive approach and take control over the user data stored in our database. It is assumed in this tutorial that you have successfully established a connection to a database, ensuring that works correctly. The examples in this tutorial will be demonstrated using MySQL and JPA; however, you can easily adapt the instructions to your chosen stack, such as MongoDB or opting for JDBC over JPA.

Here is the Data Definition Language (DDL) employed for our users:

CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(25),
    password TEXT
)
Enter fullscreen mode Exit fullscreen mode

For managing database migrations, I recommend usin a version control system such as Flyway. A detailed tutorial on Flyway can be found here.

Note that our user entries are without any extra information. This tutorial doesn't require additional details, but feel free to add any fields you need (like email, street, phone). It's essential to emphasize that this schema doesn't include any role (the authorization part of Spring Security) as it's a topic not covered in this tutorial.

With all our configuration placed in the security package before, let's proceed to create a new package called user to establish a User model.

User.kt

@Entity
@Table(name = "users")
class User(
    @Column(nullable = false)
    private val username: String,
    private val password: String
): UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0


    override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
        return mutableListOf(SimpleGrantedAuthority("USER"))
    }

    override fun getPassword(): String {
        return password
    }

    override fun getUsername(): String {
        return username
    }

    override fun isAccountNonExpired(): Boolean {
        return true
    }

    override fun isAccountNonLocked(): Boolean {
        return true
    }

    override fun isCredentialsNonExpired(): Boolean {
        return true
    }

    override fun isEnabled(): Boolean {
        return true
    }

}
Enter fullscreen mode Exit fullscreen mode

This model is annotated with @Entity, and I've specified the @Table name. I added the username and password in the primary constructor, while keeping the ID outside of it and initialized it as 0. The database will manage the ID, and it's not our responsibility to manage this field. If a new User is created, it can go straight as User("username", "password") rather than User(null, "username", "password").

This class implements the UserDetails interface, which establishes the connection needed between the security framework and your model. The methods implemented are a requirement from this interface.

For getPassword and getUsername, we provide this information, allowing the AuthenticationFilter to retrieve the info when needed. Regarding getAuthorities, it's related to the roles a user can have for the authority part. Since we're not covering this topic in these articles, we simply return a list with a unique role called "USER."

All the methods starting with is* are eventually called by the filter chain to check user information for granting authentication or authorization. For simplicity, all of them return true, and this topic is not explored in the tutorial. As an example, consider isAccountNonLocked; we could record in the database the wrong login attempts for the user. If it exceeds a certain threshold, we could return false, locking the user account and allowing login after a specified period.

With UserDetails implemented, it's time to create a repository that connects the model and the database. In the user package:

UserRepository.kt

Interface UserRepository: ListCrudRepository<User, Long> {
    fun findByUsername(username: String): Optional<User>
}
Enter fullscreen mode Exit fullscreen mode

With JPA 3.X, the interfaces from JPA have slight differences, and ListCrudRepository now implements the same methods as CrudRepository but returns a List type instead of Iterable. Here is the schema of interfaces for JPA 3:

JPA 3 interfaces

Now, the last part of our connection with Spring Security is missing: how the filters can find the user from our DB. We have the repository, but there's no implementation to let Spring Security know that it should use this repository for finding users. Here comes our last file in the user package:

Here's the final part of our user configuration for Spring Security: the UserDetailsService implementation.

UserService.kt

@Service
class UserService(private val userRepository: UserRepository) :UserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        return userRepository.findByUsername(username).orElseThrow { RuntimeException("User Not Found") }
    }
}
Enter fullscreen mode Exit fullscreen mode

This class implements the UserDetailsService, requiring the implementation of the loadUserByUsername function. In this method, we inject our repository and use the findByUsername method we created. If the user is not found, we throw a new error.

One last detail remains to be addressed. In the config file, we previously created an in-memory user:

SecurityConfig.kt

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

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

We can now replace this in-memory user configuration with our new database-backed configuration. This ensures that Spring Security will use our UserDetailsService implementation to authenticate users against the database.

Great job👏! It's wonderful to see that the new user authentication system is working successfully with users stored in the database.

My created user in the database

When I try to log in, I get a successful response:

Successful login via thundercloud from users in the database

In this section, we learned:

  • About UserDetails and UserDetailsService
  • How to implement them correctly
  • Why do we have all the methods implementations on UserDetails, even though we are not using them

Conclusions

sum up, we've accomplished the development of a user authentication system using Spring Security, integrating JWT to ensure secure and smooth user authorization. Despite the intricate nature of Spring Security, this tutorial has been designed to provide detailed explanations at each stage, helping to get a deeper comprehension of the implementation process.

For those looking to delve deeper into Spring Security, here are some suggested next steps:

By using these guides and learning more about Spring Security, you'll be ready to make your applications solidly secure 🦸🏻‍♀️. If you ever have questions or need help, just ask. Have fun coding! 💻

Top comments (0)