Forráskód Böngészése

Add coroutine support to pre/post authorize

Closes gh-8143
Eleftheria Stein 4 éve
szülő
commit
e03fe7f089

+ 2 - 0
config/spring-security-config.gradle

@@ -85,6 +85,8 @@ dependencies {
 	testImplementation ('org.springframework.data:spring-data-jpa') {
 		exclude group: 'org.aspectj', module: 'aspectjrt'
 	}
+	testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
+	testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor'
 
 	testRuntimeOnly 'org.hsqldb:hsqldb'
 }

+ 3 - 2
config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -77,7 +77,8 @@ public class EnableReactiveMethodSecurityTests {
 				.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
 						+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
 						+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
-						+ ".Publisher (i.e. Mono / Flux) in order to support Reactor Context");
+						+ ".Publisher (i.e. Mono / Flux) or the function must be a Kotlin coroutine "
+						+ "function in order to support Reactor Context");
 	}
 
 	@Test

+ 186 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityTests.kt

@@ -0,0 +1,186 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.method.configuration
+
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.test.context.support.WithMockUser
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit4.SpringRunner
+
+@RunWith(SpringRunner::class)
+@ContextConfiguration
+class KotlinEnableReactiveMethodSecurityTests {
+
+    @Autowired
+    var messageService: KotlinReactiveMessageService? = null
+
+    @Test
+    fun suspendingGetResultWhenPermitAllThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingNoAuth()).isEqualTo("success")
+        }
+    }
+
+    @Test
+    @WithMockUser(authorities = ["ROLE_ADMIN"])
+    fun suspendingPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingPreAuthorizeHasRole()).isEqualTo("admin")
+        }
+    }
+
+    @Test
+    fun suspendingPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.suspendingPreAuthorizeHasRole()
+            }
+        }
+    }
+
+    @Test
+    @WithMockUser
+    fun suspendingPreAuthorizeBeanWhenGrantedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingPreAuthorizeBean(true)).isEqualTo("check")
+        }
+    }
+
+    @Test
+    fun suspendingPreAuthorizeBeanWhenNotAuthorizedThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.suspendingPreAuthorizeBean(false)
+            }
+        }
+    }
+
+    @Test
+    @WithMockUser("user")
+    fun suspendingPostAuthorizeWhenAuthorizedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingPostAuthorizeContainsName()).isEqualTo("user")
+        }
+    }
+
+    @Test
+    @WithMockUser("other-user")
+    fun suspendingPostAuthorizeWhenNotAuthorizedThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.suspendingPostAuthorizeContainsName()
+            }
+        }
+    }
+
+    @Test
+    @WithMockUser(authorities = ["ROLE_ADMIN"])
+    fun suspendingFlowPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingFlowPreAuthorize().toList()).containsExactly(1, 2, 3)
+        }
+    }
+
+    @Test
+    fun suspendingFlowPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.suspendingFlowPreAuthorize().collect()
+            }
+        }
+    }
+
+    @Test
+    fun suspendingFlowPostAuthorizeWhenAuthorizedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.suspendingFlowPostAuthorize(true).toList()).containsExactly(1, 2, 3)
+        }
+    }
+
+    @Test
+    fun suspendingFlowPostAuthorizeWhenNotAuthorizedThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.suspendingFlowPostAuthorize(false).collect()
+            }
+        }
+    }
+
+    @Test
+    @WithMockUser(authorities = ["ROLE_ADMIN"])
+    fun flowPreAuthorizeHasRoleWhenGrantedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.flowPreAuthorize().toList()).containsExactly(1, 2, 3)
+        }
+    }
+
+    @Test
+    fun flowPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.flowPreAuthorize().collect()
+            }
+        }
+    }
+
+    @Test
+    fun flowPostAuthorizeWhenAuthorizedThenSuccess() {
+        runBlocking {
+            assertThat(messageService!!.flowPostAuthorize(true).toList()).containsExactly(1, 2, 3)
+        }
+    }
+
+    @Test
+    fun flowPostAuthorizeWhenNotAuthorizedThenDenied() {
+        assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
+            runBlocking {
+                messageService!!.flowPostAuthorize(false).collect()
+            }
+        }
+    }
+
+    @EnableReactiveMethodSecurity
+    @Configuration
+    open class Config {
+
+        @Bean
+        open fun messageService(): KotlinReactiveMessageServiceImpl {
+            return KotlinReactiveMessageServiceImpl()
+        }
+
+        @Bean
+        open fun authz(): Authz {
+            return Authz()
+        }
+
+        open class Authz {
+            fun check(r: Boolean): Boolean {
+                return r
+            }
+        }
+    }
+}

+ 38 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageService.kt

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.method.configuration
+
+import kotlinx.coroutines.flow.Flow
+
+interface KotlinReactiveMessageService {
+
+    suspend fun suspendingNoAuth(): String
+
+    suspend fun suspendingPreAuthorizeHasRole(): String
+
+    suspend fun suspendingPreAuthorizeBean(id: Boolean): String
+
+    suspend fun suspendingPostAuthorizeContainsName(): String
+
+    suspend fun suspendingFlowPreAuthorize(): Flow<Int>
+
+    suspend fun suspendingFlowPostAuthorize(id: Boolean): Flow<Int>
+
+    fun flowPreAuthorize(): Flow<Int>
+
+    fun flowPostAuthorize(id: Boolean): Flow<Int>
+}

+ 91 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageServiceImpl.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.method.configuration
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import org.springframework.security.access.prepost.PostAuthorize
+import org.springframework.security.access.prepost.PreAuthorize
+
+class KotlinReactiveMessageServiceImpl : KotlinReactiveMessageService {
+
+    override suspend fun suspendingNoAuth(): String {
+        delay(1)
+        return "success"
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    override suspend fun suspendingPreAuthorizeHasRole(): String {
+        delay(1)
+        return "admin"
+    }
+
+    @PreAuthorize("@authz.check(#id)")
+    override suspend fun suspendingPreAuthorizeBean(id: Boolean): String {
+        delay(1)
+        return "check"
+    }
+
+    @PostAuthorize("returnObject?.contains(authentication?.name)")
+    override suspend fun suspendingPostAuthorizeContainsName(): String {
+        delay(1)
+        return "user"
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    override suspend fun suspendingFlowPreAuthorize(): Flow<Int> {
+        delay(1)
+        return flow {
+            for (i in 1..3) {
+                delay(1)
+                emit(i)
+            }
+        }
+    }
+
+    @PostAuthorize("@authz.check(#id)")
+    override suspend fun suspendingFlowPostAuthorize(id: Boolean): Flow<Int> {
+        delay(1)
+        return flow {
+            for (i in 1..3) {
+                delay(1)
+                emit(i)
+            }
+        }
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    override fun flowPreAuthorize(): Flow<Int> {
+        return flow {
+            for (i in 1..3) {
+                delay(1)
+                emit(i)
+            }
+        }
+    }
+
+    @PostAuthorize("@authz.check(#id)")
+    override fun flowPostAuthorize(id: Boolean): Flow<Int> {
+        return flow {
+            for (i in 1..3) {
+                delay(1)
+                emit(i)
+            }
+        }
+    }
+}

+ 1 - 0
core/spring-security-core.gradle

@@ -26,6 +26,7 @@ dependencies {
 	optional 'org.aspectj:aspectjrt'
 	optional 'org.springframework:spring-jdbc'
 	optional 'org.springframework:spring-tx'
+	optional 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor'
 
 	testImplementation powerMock2Dependencies
 	testImplementation 'commons-collections:commons-collections'

+ 74 - 4
core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,9 @@ package org.springframework.security.access.prepost;
 import java.lang.reflect.Method;
 import java.util.Collection;
 
+import kotlin.coroutines.Continuation;
+import kotlinx.coroutines.reactive.AwaitKt;
+import kotlinx.coroutines.reactive.ReactiveFlowKt;
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
 import org.reactivestreams.Publisher;
@@ -26,6 +29,11 @@ import reactor.core.Exceptions;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
+import org.springframework.core.CoroutinesUtils;
+import org.springframework.core.KotlinDetector;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ReactiveAdapter;
+import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.ConfigAttribute;
 import org.springframework.security.access.method.MethodSecurityMetadataSource;
@@ -38,9 +46,11 @@ import org.springframework.util.Assert;
 
 /**
  * A {@link MethodInterceptor} that supports {@link PreAuthorize} and
- * {@link PostAuthorize} for methods that return {@link Mono} or {@link Flux}
+ * {@link PostAuthorize} for methods that return {@link Mono} or {@link Flux} and Kotlin
+ * coroutine functions.
  *
  * @author Rob Winch
+ * @author Eleftheria Stein
  * @since 5.0
  */
 public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor {
@@ -54,6 +64,10 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
 
 	private final PostInvocationAuthorizationAdvice postAdvice;
 
+	private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
+
+	private static final int RETURN_TYPE_METHOD_PARAMETER_INDEX = -1;
+
 	/**
 	 * Creates a new instance
 	 * @param attributeSource the {@link MethodSecurityMetadataSource} to use
@@ -75,10 +89,18 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
 	public Object invoke(final MethodInvocation invocation) {
 		Method method = invocation.getMethod();
 		Class<?> returnType = method.getReturnType();
-		Assert.state(Publisher.class.isAssignableFrom(returnType),
+
+		boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
+		boolean hasFlowReturnType = COROUTINES_FLOW_CLASS_NAME
+				.equals(new MethodParameter(method, RETURN_TYPE_METHOD_PARAMETER_INDEX).getParameterType().getName());
+		boolean hasReactiveReturnType = Publisher.class.isAssignableFrom(returnType) || isSuspendingFunction
+				|| hasFlowReturnType;
+
+		Assert.state(hasReactiveReturnType,
 				() -> "The returnType " + returnType + " on " + method
 						+ " must return an instance of org.reactivestreams.Publisher "
-						+ "(i.e. Mono / Flux) in order to support Reactor Context");
+						+ "(i.e. Mono / Flux) or the function must be a Kotlin coroutine "
+						+ "function in order to support Reactor Context");
 		Class<?> targetClass = invocation.getThis().getClass();
 		Collection<ConfigAttribute> attributes = this.attributeSource.getAttributes(method, targetClass);
 		PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);
@@ -98,6 +120,30 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
 			return toInvoke.flatMapMany((auth) -> PrePostAdviceReactiveMethodInterceptor.<Flux<?>>proceed(invocation)
 					.map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r));
 		}
+		if (hasFlowReturnType) {
+			Publisher<?> publisher;
+			if (isSuspendingFunction) {
+				publisher = CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(),
+						invocation.getArguments());
+			}
+			else {
+				ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnType);
+				Assert.state(adapter != null, () -> "The returnType " + returnType + " on " + method
+						+ " must have a org.springframework.core.ReactiveAdapter registered");
+				publisher = adapter.toPublisher(PrePostAdviceReactiveMethodInterceptor.flowProceed(invocation));
+			}
+			Flux<?> response = toInvoke.flatMapMany((auth) -> Flux.from(publisher)
+					.map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r));
+			return KotlinDelegate.asFlow(response);
+		}
+		if (isSuspendingFunction) {
+			Mono<?> response = toInvoke.flatMap((auth) -> Mono
+					.from(CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(),
+							invocation.getArguments()))
+					.map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r));
+			return KotlinDelegate.awaitSingleOrNull(response,
+					invocation.getArguments()[invocation.getArguments().length - 1]);
+		}
 		return toInvoke.flatMapMany(
 				(auth) -> Flux.from(PrePostAdviceReactiveMethodInterceptor.<Publisher<?>>proceed(invocation))
 						.map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r));
@@ -112,6 +158,15 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
 		}
 	}
 
+	private static Object flowProceed(final MethodInvocation invocation) {
+		try {
+			return invocation.proceed();
+		}
+		catch (Throwable throwable) {
+			throw Exceptions.propagate(throwable);
+		}
+	}
+
 	private static PostInvocationAttribute findPostInvocationAttribute(Collection<ConfigAttribute> config) {
 		for (ConfigAttribute attribute : config) {
 			if (attribute instanceof PostInvocationAttribute) {
@@ -130,4 +185,19 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor
 		return null;
 	}
 
+	/**
+	 * Inner class to avoid a hard dependency on Kotlin at runtime.
+	 */
+	private static class KotlinDelegate {
+
+		private static Object asFlow(Publisher<?> publisher) {
+			return ReactiveFlowKt.asFlow(publisher);
+		}
+
+		private static Object awaitSingleOrNull(Publisher<?> publisher, Object continuation) {
+			return AwaitKt.awaitSingleOrNull(publisher, (Continuation<Object>) continuation);
+		}
+
+	}
+
 }

+ 1 - 0
dependencies/spring-security-dependencies.gradle

@@ -12,6 +12,7 @@ dependencies {
 	api platform("io.rsocket:rsocket-bom:1.1.0")
 	api platform("org.springframework.data:spring-data-bom:2020.0.7")
 	api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
+	api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.3")
 	api platform("com.fasterxml.jackson:jackson-bom:2.12.2")
 	constraints {
 		api "ch.qos.logback:logback-classic:1.2.3"

+ 128 - 6
docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc

@@ -6,11 +6,13 @@ For example, this demonstrates how to retrieve the currently logged in user's me
 
 [NOTE]
 ====
-For this to work the return type of the method must be a `org.reactivestreams.Publisher` (i.e. `Mono`/`Flux`).
+For this to work the return type of the method must be a `org.reactivestreams.Publisher` (i.e. `Mono`/`Flux`) or the function must be a Kotlin coroutine function.
 This is necessary to integrate with Reactor's `Context`.
 ====
 
-[source,java]
+====
+.Java
+[source,java,role="primary"]
 ----
 Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
 
@@ -26,18 +28,48 @@ StepVerifier.create(messageByUsername)
 	.verifyComplete();
 ----
 
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")
+
+val messageByUsername: Mono<String> = ReactiveSecurityContextHolder.getContext()
+	.map(SecurityContext::getAuthentication)
+	.map(Authentication::getName)
+	.flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
+	.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
+
+StepVerifier.create(messageByUsername)
+	.expectNext("Hi user")
+	.verifyComplete()
+----
+====
+
 with `this::findMessageByUsername` defined as:
 
-[source,java]
+====
+.Java
+[source,java,role="primary"]
 ----
 Mono<String> findMessageByUsername(String username) {
 	return Mono.just("Hi " + username);
 }
 ----
 
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+fun findMessageByUsername(username: String): Mono<String> {
+	return Mono.just("Hi $username")
+}
+----
+====
+
 Below is a minimal method security configuration when using method security in reactive applications.
 
-[source,java]
+====
+.Java
+[source,java,role="primary"]
 ----
 @EnableReactiveMethodSecurity
 public class SecurityConfig {
@@ -57,9 +89,33 @@ public class SecurityConfig {
 }
 ----
 
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableReactiveMethodSecurity
+class SecurityConfig {
+	@Bean
+	fun userDetailsService(): MapReactiveUserDetailsService {
+		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
+		val rob = userBuilder.username("rob")
+			.password("rob")
+			.roles("USER")
+			.build()
+		val admin = userBuilder.username("admin")
+			.password("admin")
+			.roles("USER", "ADMIN")
+			.build()
+		return MapReactiveUserDetailsService(rob, admin)
+	}
+}
+----
+====
+
 Consider the following class:
 
-[source,java]
+====
+.Java
+[source,java,role="primary"]
 ----
 @Component
 public class HelloWorldMessageService {
@@ -70,6 +126,37 @@ public class HelloWorldMessageService {
 }
 ----
 
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Component
+class HelloWorldMessageService {
+	@PreAuthorize("hasRole('ADMIN')")
+	fun findMessage(): Mono<String> {
+		return Mono.just("Hello World!")
+	}
+}
+----
+====
+
+Or, the following class using Kotlin coroutines:
+
+====
+.Kotlin
+[source,kotlin,role="primary"]
+----
+@Component
+class HelloWorldMessageService {
+    @PreAuthorize("hasRole('ADMIN')")
+    suspend fun findMessage(): String {
+        delay(10)
+        return "Hello World!"
+    }
+}
+----
+====
+
+
 Combined with our configuration above, `@PreAuthorize("hasRole('ADMIN')")` will ensure that `findByMessage` is only invoked by a user with the role `ADMIN`.
 It is important to note that any of the expressions in standard method security work for `@EnableReactiveMethodSecurity`.
 However, at this time we only support return type of `Boolean` or `boolean` of the expression.
@@ -77,7 +164,9 @@ This means that the expression must not block.
 
 When integrating with <<jc-webflux>>, the Reactor Context is automatically established by Spring Security according to the authenticated user.
 
-[source,java]
+====
+.Java
+[source,java,role="primary"]
 ----
 @EnableWebFluxSecurity
 @EnableReactiveMethodSecurity
@@ -112,4 +201,37 @@ public class SecurityConfig {
 
 ----
 
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableWebFluxSecurity
+@EnableReactiveMethodSecurity
+class SecurityConfig {
+	@Bean
+	open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+		return http {
+			authorizeExchange {
+				authorize(anyExchange, permitAll)
+			}
+			httpBasic { }
+		}
+	}
+
+	@Bean
+	fun userDetailsService(): MapReactiveUserDetailsService {
+		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
+		val rob = userBuilder.username("rob")
+			.password("rob")
+			.roles("USER")
+			.build()
+		val admin = userBuilder.username("admin")
+			.password("admin")
+			.roles("USER", "ADMIN")
+			.build()
+		return MapReactiveUserDetailsService(rob, admin)
+	}
+}
+----
+====
+
 You can find a complete sample in {gh-samples-url}/javaconfig/hellowebflux-method[hellowebflux-method]