I started my career as a developer in 2011. Soon I came upon the problem of a user proving who they are, and what they’re allowed to do. Seemed hard, but I reasoned I would get the hang of it quickly.
That didn’t really happen. Authentication is hard. Authorization is harder.
But. New tools and services make things easier. And today I will share a tiny crazy Kotlin tidbit that made my day a bit easier.
3 min read
·
By Geir Sagberg
·
December 15, 2023
Spring is the most popular web framework in the JVM sphere, and of course it comes with its own security suite. Now, being a Java framework, it uses builders for configuration, which looks something like this:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class FormLoginSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/**")
.hasRole("USER"));
http.formLogin(formLogin -> {
formLogin.loginPage("/login");
});
return http.build();
}
}
Not that horrible, really. But we can do better. We are writing in Kotlin, after all.
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/", hasRole("USER"))
}
formLogin {
loginPage = "/login"
}
}
return http.build()
}
}
Tiny bit cleaner, right? But what is actually going on here? Did someone re-write the entire Spring Security Stack to work in a Kotlin-idiomatic way? Looking at the imports, it really doesn’t seem like it. No, the genius culprit is this little line here:
import org.springframework.security.config.annotation.web.invoke
Digging into the definition leads us here:
operator fun HttpSecurity.invoke(httpConfiguration: HttpSecurityDsl.() -> Unit) =
HttpSecurityDsl(this, httpConfiguration).build()
For those unfamiliar with Kotlin (and indeed many Kotlin users) this might seem daunting. However, this is an idiomatic way to create a Domain Specific Language (DSL) in Kotlin. Let’s unpack it:
operator fun
: This means we are overloading an operator, i.e. a built-in part of the language.HttpSecurity.invoke
: This means we are overloading the built-in invoke
function, but prefixed with HttpSecurity
as its receiver, creating a function that can be called as if it existed as a member function of HttpSecurity
. invoke
function is the same as calling a function with parentheses. httpConfiguration: HttpSecurityDsl.() -> Unit
: The invoke
function takes in a function with a receiver of type HttpSecurityDsl
. Meaning for whatever function we pass in, this
will be of the type HttpSecurityDsl
.HttpSecurityDsl(this, httpConfiguration).build()
: The invoke function
returns an instance of HttpSecurityDsl
, passing in the HttpSecurity
object and the httpConfiguration
function.We'll look at the HttpSecurityDsl
in a bit, but first I want to focus on exactly what it means that we can overload the invoke()
function:
Think of how in English, everything can be verbed: To cat around; I goblined at home this weekend; did you bus, bike, rollerblade or foot it here?
Well, overloading invoke()
is kind of like that. Here is an example:
abstract class Animal {
abstract val name: String
}
class Cat(override val name: String) : Animal()
class Dog(override val name: String) : Animal()
operator fun Animal.invoke() {
println(name + " doing " + javaClass.simpleName + " stuff")
}
val mittens = Cat("Mittens")
mittens.invoke()
// Prints: Mittens doing Cat stuff
val fido = Dog("Fido")
fido()
// Prints: Fido doing Dog stuff
We can invoke the animals as if they were functions, either by calling .invoke()
or by just adding some parentheses. Pretty cool and/or crazy, right?
So what is this mystical HttpSecurityDsl
type? If we examine it closer, we find dozens of methods that match closely to the builder methods of the original HttpSecurity
. Here is an example:
fun authorizeHttpRequests(authorizeHttpRequestsConfiguration: AuthorizeHttpRequestsDsl.() -> Unit) {
val authorizeHttpRequestsCustomizer = AuthorizeHttpRequestsDsl().apply(authorizeHttpRequestsConfiguration).get()
this.http.authorizeHttpRequests(authorizeHttpRequestsCustomizer)
}
So here another DSL is started, for specifically configuring HTTP Requests. And this is the basic pattern for nesting configurations, as we did in the securityFilterChain
function above.
Creating nested DSLs in this way might seem complicated, but the end result is an extremely clean and readable syntax for building anything from security configurations to type-safe HTML.
So, with the tools of operator overloading, function types with receivers, trailing lambdas and DSL classes, we have extended a regular Java object http
of the class HttpSecurity
into the starting point of a Kotlin-idiomatic domain specific language for configuring our authentication and authorization. I think that’s pretty crazy.
Start writing your own DSL today, or take a look at Type-safe builders if you want more examples. Start verbing today!