Hopp til hovedinnhold

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.

Spring Security, Kotlin and DSLs

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.
    • The invoke function is the same as calling a function with parentheses.
    • When the only parameter of a function is another function, we don’t need the parentheses at all, and can use only curly braces. This is known as a trailing lambda.
  • 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:

Any instance of the type is turned into a callable 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?

DSL builders

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.

Conclusion

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!

Did you like the post?

Feel free to share it with friends and colleagues