Scratch the edge using Spring WebFlux (Part 2)

Iman
3 min readDec 23, 2020

--

Photo by Jonathan Cooper on Unsplash

We know Bob from the previous story and he wants to use his service right now, and he could do it but wait! this is his exclusive service, is he alone right now?
He and the whole world can use this service forever. let’s limit the alien’s access by Spring Security.

First thing first, we need a model for our users (lovely Bob is alone) so let’s create one as the follow in kotlin>com>{something you know}>{project name, that also you know}>model

@Document
data class User(
@Id
val id : ObjectId,
@Indexed(unique = true)
val name: String,
val pass: String,
val role: String
) : UserDetails {

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

override fun getPassword(): String {
return pass
}

override fun getUsername(): String {
return name
}

override fun isAccountNonExpired() = true

override fun isAccountNonLocked() = true

override fun isCredentialsNonExpired() = true

override fun isEnabled() = true
}

You can see two elegant things in one frame, the flexibility of Spring Security and the conciseness of Kotlin (by comparing two different types of method).

# Note that you can’t use `username` and `password` fields in this special data model because of some getter and setter conflicts.

And in kotlin>com>{something you know}>{project name, that also you know}>repository

interface UserRepository : ReactiveMongoRepository<User,ObjectId>{
fun findByName(name: String) : Mono<User>
}

There is a problem before continuing, Bob is too lazy. He uses the same password everywhere then, we have to protect his password by saving an encrypted version of his password, following class will help us

@Configuration
class PasswordEncoderConfig {

@Bean
fun getEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}

Now, we need an authentication manager, it simply checks users claim and let them continue if true, so create `AuthenticationManager` as follow in kotlin>com>{something you know}>{project name, that also you know}>security

@Component
class AuthenticationManager(private val encoder: PasswordEncoder,
private val userRepository: UserRepository) : ReactiveAuthenticationManager {
override fun authenticate(authentication: Authentication): Mono<Authentication> {
val pass = authentication.credentials.toString()
val name = authentication.principal.toString()

return userRepository.findByName(name).filter { user ->
encoder.matches(pass, user.password)
}.map { user ->
UsernamePasswordAuthenticationToken(user.name, user.pass, user.authorities)
}
}
}

Let’s prepare and play with our authentication manager in Spring Security as follow in kotlin>com>{something you know}>{project name, that also you know}>security

@Component
class SecurityContextChain(private val authenticationManager: AuthenticationManager) : ServerSecurityContextRepository {
override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> {
throw UnsupportedOperationException("Not Supported yet ...")
}

override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
val request = exchange.request
val authHeader = request.headers.getFirst(HttpHeaders.AUTHORIZATION)
return if (authHeader != null && authHeader.length > 6 && authHeader.startsWith("Basic ")) {
// split token part
val token = authHeader.substring(6)
// decode the token
val bytes = Base64.getDecoder().decode(token)
// convert decoded token to Sting
val credentials = String(bytes, StandardCharsets.UTF_8)
// split username and password
val values = credentials.split(Regex(":"), 2)

val auth = UsernamePasswordAuthenticationToken(values[0],values[1])

authenticationManager.authenticate(auth).map { authentication ->
SecurityContextImpl(authentication)
}
} else {
Mono.empty()
}
}
}

It gets the auth header from the request, prepare it (check, decode, split) and pass it to our Authentication Manager.

Now we need to configure Spring Security as follow in kotlin>com>{something you know}>{project name, that also you know}>security

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig(private val authenticationManager: ReactiveAuthenticationManager,
private val securityContextRepository: ServerSecurityContextRepository) {
@Bean
fun setSecurityConfigure(http : ServerHttpSecurity) : SecurityWebFilterChain{
return http
.formLogin()1️⃣
.disable()
.authenticationManager(authenticationManager)2️⃣
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange()3️⃣
.authenticated()
.and()
.build()

}
}
  1. We’ve disabled the login page (create by default by Spring Security)
  2. Introduced our auth staffs
  3. And “Please, secure all of my endpoints” we’ve told Spring Security

It’s done, our application is secure now, but anyone can not use it even Bob! we need to include Bob as our users as follow in kotlin>com>{something you know}>{project name, that also you know}>extra

@Component
class InitBean(private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder) : CommandLineRunner {

private val userName = "Bob"
private val password = "BobLikesBobWithDoubleO"

override fun run(vararg args: String?) {
userRepository.findByName(userName).switchIfEmpty {
val user = User(ObjectId.get(), userName, passwordEncoder.encode(password),"USER")
userRepository.save(user)
}.subscribe()
}

}

Now Bob can request and enjoy his response with his username and password.

# You can find source code on Github

# You can find the previous story, here.

--

--