|

Spring Security & WebFlux: Load Users from a Database

In the last episode we established a simple form-based authentication with hardcoded credentials. Today, we take it step further and load user details from a database.

Project Setup

If you are new to this series on Spring Security and WebFlux, check my initial blog post to see how to start from scratch.

Let’s add a couple of dependencies.

Spring Data R2DBC provides database connectivity in non-blocking manner. It adheres to the R2DBC specification, which supports reactive streams. Whether this actually pays off in production or not is a different question. This blog post (March 2020!) brings interesting comparison and insights. Nevertheless, since my project runs on WebFlux I want to leverage the technology as much as I can and only switch to JDBC if I find a clear setback with R2DBC.

H2 Database is a light-weight in-memory database that requires minimum to zero configuration.

You can add both of these through Gradle (with Kotlin DSL):

  runtimeOnly("com.h2database:h2")
  runtimeOnly("io.r2dbc:r2dbc-h2")

Another convenient way of adding the dependencies is via Spring Initializr if that’s what you prefer.

Today’s Takeaway

  • Learn how to connect your application to a database
  • Create a custom user service and attach it to the authentication flow
  • Add as many users with different user roles as you wish

All of that in only seven simple steps:

  1. Create a users table
  2. Create a user entity
  3. Implement a user lookup
  4. Transform the user entity into user details
  5. Add a custom user details service
  6. Update the configuration
  7. Update the frontend

.. and enjoy the fruits of your labor!

A complete example is available on GitLab.

Follow the video tutorial below for a step-by-step guidance.

Step 1: Create a Users Table

Under src/main/resources in your project directory add a new script called data.sql.

DROP TABLE IF EXISTS users;

CREATE TABLE users
(
    username       VARCHAR(50) PRIMARY KEY,
    password       VARCHAR(250) NOT NULL,
    user_role      VARCHAR(50) NOT NULL,
    first_name     VARCHAR(100) NOT NULL,
    last_name      VARCHAR(100) NOT NULL,
    email          VARCHAR(100) NOT NULL UNIQUE,
    avatar         VARCHAR,
    secret         VARCHAR,
    account_enabled BOOLEAN NOT NULL DEFAULT true,
    account_expired BOOLEAN NOT NULL DEFAULT false,
    account_locked BOOLEAN NOT NULL DEFAULT false,
    credentials_expired BOOLEAN NOT NULL DEFAULT false,
    use_2fa BOOLEAN NOT NULL DEFAULT false
);

INSERT INTO users (username, password, user_role, first_name, last_name, email) VALUES ('bob', '$2a$10$esolmUvFZDqSIE744dU5V.5dPxBk0.xzjDXe7Gim4tou7DXYBLa4q', 'USER', 'Bob', 'Doe', 'bob@doe.com');
INSERT INTO users (username, password, user_role, first_name, last_name, email) VALUES ('alice', '$2a$10$esolmUvFZDqSIE744dU5V.5dPxBk0.xzjDXe7Gim4tou7DXYBLa4q', 'ADMIN', 'Alice', 'Doe', 'alice@doe.com');
COMMIT;

No configuration is required. Spring will discover this script and run it when the application starts up. Beware that dropping a table with every single restart is a bad idea in production! However, in a hobby project like this one it turns into a feature – any schema changes will be automatically reflected with every new restart. Note that user passwords are encrypted. It is a good practice to encrypt your data at rest.

Step 2: Create a User Entity

Create a new package, I like to call it model, but other common names are domain or entities. Feel free to call it whatever you like. In this new package, I create a class that represents a user record in the database. Since I have added annotations that map the class’ fields to database columns, It’s no longer just any old class, it’s an entity. Meaning, Spring will know to map database records to user objects in my code.

package com.langnerd.webfluxsecurity.model

import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table

@Table("users")
data class User(
    @Id
    val username: String,
    
    val password: String,

    @Column("user_role")
    val userRole: String,

    @Column("first_name")
    val firstName: String,

    @Column("last_name")
    val lastName: String,

    val email: String,

    @Column("account_enabled")
    val accountEnabled: Boolean,

    @Column("account_expired")
    val accountExpired: Boolean,

    @Column("account_locked")
    val accountLocked: Boolean,

    @Column("credentials_expired")
    val credentialsExpired: Boolean
)

Step 3: Implement a User Lookup

On the login page, a user enters her username and password. In order to conclude whether the provided credentials are valid Spring Security has to do two things.

  1. Take the username and load the corresponding user object from the database
  2. Take the provided password, encrypt it and compare the result with the encrypted password loaded from the database.

Essentially, it all starts with looking up a user from the database.

Create a new package, I like to call it dao as in Data Access Object, but feel free to give it a different name – such as repository or anything else you like and find intuitive.

Add a new Spring Data repository which knows how to find a user by a username.

package com.langnerd.webfluxsecurity.dao

import com.langnerd.webfluxsecurity.model.User
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import reactor.core.publisher.Mono

interface UserRepository : ReactiveCrudRepository<User, String> {
    
    @Query("SELECT * FROM users WHERE LOWER(username) = LOWER(:username)")
    fun findByUsername(username: String): Mono<User>
}

Step 4: Transform the User Entity into a UserDetails Object

Spring Security doesn’t give a damn about our user entity. The only user representation the frameworks is aware of is UserDetails.

The problem is that UserDetails class does not provide any custom user fields. So, let’s create our own implementation.

Create a new package, I call it dto as in Data Transfer Object, but feel free to name it differently. In this new package let’s add a new class named UsernamePasswordPrincipal. Why this name? Well, think about it. In the future, you will want to add some sort of a password-less authentication. Such as single sign-on through Google or Facebook. That’s why it’s a good practice to be clear with the purpose a custom class of yours serves. Right now, it’s all about username and password, let’s make it obvious. Note the custom fields that store information about user’s first and last name and her email address.

package com.langnerd.webfluxsecurity.dto

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails

class UsernamePasswordPrincipal(
    private val username: String,
    private val password: String,
    private val role: String,
    private val accountExpired: Boolean,
    private val accountLocked: Boolean,
    private val credentialsExpired: Boolean,
    private val enabled: Boolean,
    val firstName: String,
    val lastName: String,
    val email: String
) :
    UserDetails {

    override fun getAuthorities(): MutableCollection<GrantedAuthority> =
        setOf(role).map(::SimpleGrantedAuthority).toMutableSet()

    override fun getPassword(): String = password

    override fun getUsername(): String = username

    override fun isAccountNonExpired(): Boolean = !accountExpired

    override fun isAccountNonLocked(): Boolean = !accountLocked

    override fun isCredentialsNonExpired(): Boolean = !credentialsExpired

    override fun isEnabled(): Boolean = enabled
}

Let’s add an extension function to our User entity that will convert it into the UsernamePasswordPrincipal object.

fun User.toUsernamePasswordPrincipal(): UsernamePasswordPrincipal =
    UsernamePasswordPrincipal(
        this.username,
        this.password,
        this.userRole,
        this.accountExpired,
        this.accountLocked,
        this.credentialsExpired,
        this.accountEnabled,
        this.firstName,
        this.lastName,
        this.email
    )

Step 5: Add a Custom UserDetailsService

The last missing piece into the puzzle is having a custom implementation of Spring Security’s UserDetailsService that returns user details enriched with additional information.

Create a new package, let’s call it service. Add a new implementation of UserDetailsService. In fact, since we work with a reactive web application we make use of a reactive version.

The logic is straightforward. Internally, the service uses our user repository to look up a user by username. Next, it transforms the found user entity into the expected user details object.

package com.langnerd.webfluxsecurity.service.impl

import com.langnerd.webfluxsecurity.dao.UserRepository
import com.langnerd.webfluxsecurity.toUsernamePasswordPrincipal
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono

@Service
class SpringUserDetailsService(private val userRepository: UserRepository) : ReactiveUserDetailsService {

    override fun findByUsername(username: String): Mono<UserDetails> =
        userRepository.findByUsername(username).map { it.toUsernamePasswordPrincipal() }
}

Step 6: Update the Configuration

All we need to do now is to change the configuration.

More specifically, we need to add a custom AuthenticationManager which in turn uses our custom UserDetailsService. This ensures that whichever input the user submits via the login form will lead to a database lookup and will pull our custom user details into the authentication flow.

The authentication manager also must be aware of the fact that passwords are encrypted and contain the correct password encoder.

package com.langnerd.webfluxsecurity.config

import org.springframework.context.annotation.Bean
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain

t
@EnableWebFluxSecurity
class SecurityConfig(private val userDetailsService: ReactiveUserDetailsService) {

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain =
        http
            .authorizeExchange()
            .anyExchange().authenticated()
            .and().authenticationManager(authenticationManager())
            .formLogin()
            .and().build()
    
    private fun authenticationManager(): ReactiveAuthenticationManager {
        val authenticationManager = UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)
        authenticationManager.setPasswordEncoder(passwordEncoder())
        return authenticationManager
    }

    private fun passwordEncoder(): PasswordEncoder =
        BCryptPasswordEncoder(10)    
}

Step 7: Update the Frontend

We are almost done. The only thing left to make the custom user details visible.

In src/main/resources/templates let’s update the index.html as follows.

<!DOCTYPE html>
<html lang="en" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<p>Hello and welcome back, 
  <span sec:authentication="principal.firstName"></span>
  <span sec:authentication="principal.lastName"></span>
  (<span sec:authentication="principal.email"></span>)!
</p>
</body>
</html>

Enjoy the Fruits of Your Labor!

Restart the app and login as either alice or bob using a password password. Or any other credentials you’ve put into your database yourself when coding along with me. You should be rewarded with seeing first and last name of the logged in user.

Summary

In this post I showed you in a few simple steps how to add custom user information and load users from a database.

Fork the complete example on GitLab and make it your own.

Watch this episode on YouTube, give it a like, subscribe to the channel and stay tuned for my next episode where I explain how replace the default login form with your own.

Similar Posts