Jelajahi Sumber

Idiomatic Kotlin DSL for server HTTP security

Issue: gh-5558
Eleftheria Stein 5 tahun lalu
induk
melakukan
39e09e4ca5
50 mengubah file dengan 6532 tambahan dan 0 penghapusan
  1. 135 0
      config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt
  2. 61 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt
  3. 49 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt
  4. 59 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt
  5. 43 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt
  6. 85 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt
  7. 181 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt
  8. 65 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt
  9. 528 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt
  10. 70 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt
  11. 61 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt
  12. 57 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt
  13. 83 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt
  14. 118 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt
  15. 50 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt
  16. 42 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt
  17. 45 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDsl.kt
  18. 42 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDsl.kt
  19. 45 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDsl.kt
  20. 52 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDsl.kt
  21. 57 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDsl.kt
  22. 40 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDsl.kt
  23. 45 0
      config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDsl.kt
  24. 82 0
      config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDsl.kt
  25. 67 0
      config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDsl.kt
  26. 183 0
      config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt
  27. 192 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt
  28. 136 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt
  29. 210 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt
  30. 126 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt
  31. 325 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt
  32. 134 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt
  33. 219 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt
  34. 126 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt
  35. 199 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt
  36. 244 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt
  37. 223 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt
  38. 201 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt
  39. 204 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt
  40. 104 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt
  41. 237 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt
  42. 104 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDslTests.kt
  43. 125 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDslTests.kt
  44. 100 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDslTests.kt
  45. 126 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDslTests.kt
  46. 176 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDslTests.kt
  47. 101 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDslTests.kt
  48. 100 0
      config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDslTests.kt
  49. 272 0
      config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDslTests.kt
  50. 203 0
      config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDslTests.kt

+ 135 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager
+import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.authorization.ReactiveAuthorizationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.web.server.authorization.AuthorizationContext
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
+import org.springframework.security.web.util.matcher.RequestMatcher
+import reactor.core.publisher.Mono
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] exchange authorization using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class AuthorizeExchangeDsl {
+    private val authorizationRules = mutableListOf<ExchangeAuthorizationRule>()
+
+    /**
+     * Adds an exchange authorization rule for an endpoint matching the provided
+     * matcher.
+     *
+     * @param matcher the [RequestMatcher] to match incoming requests against
+     * @param access the [ReactiveAuthorizationManager] which determines the access
+     * to the specific matcher.
+     * Some predefined shortcuts have already been created, such as
+     * [hasAnyAuthority], [hasAnyRole], [permitAll], [authenticated] and more
+     */
+    fun authorize(matcher: ServerWebExchangeMatcher = ServerWebExchangeMatchers.anyExchange(),
+                  access: ReactiveAuthorizationManager<AuthorizationContext> = authenticated) {
+        authorizationRules.add(MatcherExchangeAuthorizationRule(matcher, access))
+    }
+
+    /**
+     * Adds an exchange authorization rule for an endpoint matching the provided
+     * ant pattern.
+     *
+     * @param antPattern the ant ant pattern to match incoming requests against.
+     * @param access the [ReactiveAuthorizationManager] which determines the access
+     * to the specific matcher.
+     * Some predefined shortcuts have already been created, such as
+     * [hasAnyAuthority], [hasAnyRole], [permitAll], [authenticated] and more
+     */
+    fun authorize(antPattern: String, access: ReactiveAuthorizationManager<AuthorizationContext> = authenticated) {
+        authorizationRules.add(PatternExchangeAuthorizationRule(antPattern, access))
+    }
+
+    /**
+     * Matches any exchange.
+     */
+    val anyExchange: ServerWebExchangeMatcher = ServerWebExchangeMatchers.anyExchange()
+
+    /**
+     * Allow access for anyone.
+     */
+    val permitAll: ReactiveAuthorizationManager<AuthorizationContext> =
+            ReactiveAuthorizationManager { _: Mono<Authentication>, _: AuthorizationContext -> Mono.just(AuthorizationDecision(true)) }
+
+    /**
+     * Deny access for everyone.
+     */
+    val denyAll: ReactiveAuthorizationManager<AuthorizationContext> =
+            ReactiveAuthorizationManager { _: Mono<Authentication>, _: AuthorizationContext -> Mono.just(AuthorizationDecision(false)) }
+
+    /**
+     * Require a specific role. This is a shortcut for [hasAuthority].
+     */
+    fun hasRole(role: String): ReactiveAuthorizationManager<AuthorizationContext> =
+            AuthorityReactiveAuthorizationManager.hasRole<AuthorizationContext>(role)
+
+    /**
+     * Require any specific role. This is a shortcut for [hasAnyAuthority].
+     */
+    fun hasAnyRole(vararg roles: String): ReactiveAuthorizationManager<AuthorizationContext> =
+            AuthorityReactiveAuthorizationManager.hasAnyRole<AuthorizationContext>(*roles)
+
+    /**
+     * Require a specific authority.
+     */
+    fun hasAuthority(authority: String): ReactiveAuthorizationManager<AuthorizationContext> =
+            AuthorityReactiveAuthorizationManager.hasAuthority<AuthorizationContext>(authority)
+
+    /**
+     * Require any authority.
+     */
+    fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager<AuthorizationContext> =
+            AuthorityReactiveAuthorizationManager.hasAnyAuthority<AuthorizationContext>(*authorities)
+
+    /**
+     * Require an authenticated user.
+     */
+    val authenticated: ReactiveAuthorizationManager<AuthorizationContext> =
+            AuthenticatedReactiveAuthorizationManager.authenticated<AuthorizationContext>()
+
+    internal fun get(): (ServerHttpSecurity.AuthorizeExchangeSpec) -> Unit {
+        return { requests ->
+            authorizationRules.forEach { rule ->
+                when (rule) {
+                    is MatcherExchangeAuthorizationRule -> requests.matchers(rule.matcher).access(rule.rule)
+                    is PatternExchangeAuthorizationRule -> requests.pathMatchers(rule.pattern).access(rule.rule)
+                }
+            }
+        }
+    }
+
+    private data class MatcherExchangeAuthorizationRule(val matcher: ServerWebExchangeMatcher,
+                                                        override val rule: ReactiveAuthorizationManager<AuthorizationContext>) : ExchangeAuthorizationRule(rule)
+
+    private data class PatternExchangeAuthorizationRule(val pattern: String,
+                                                        override val rule: ReactiveAuthorizationManager<AuthorizationContext>) : ExchangeAuthorizationRule(rule)
+
+    private abstract class ExchangeAuthorizationRule(open val rule: ReactiveAuthorizationManager<AuthorizationContext>)
+}
+

+ 61 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] anonymous authentication using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property key the key to identify tokens created for anonymous authentication
+ * @property principal the principal for [Authentication] objects of anonymous users
+ * @property authorities the [Authentication.getAuthorities] for anonymous users
+ * @property authenticationFilter the [AnonymousAuthenticationWebFilter] used to populate
+ * an anonymous user.
+ */
+class ServerAnonymousDsl {
+    var key: String? = null
+    var principal: Any? = null
+    var authorities: List<GrantedAuthority>? = null
+    var authenticationFilter: AnonymousAuthenticationWebFilter? = null
+
+    private var disabled = false
+
+    /**
+     * Disables anonymous authentication
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.AnonymousSpec) -> Unit {
+        return { anonymous ->
+            key?.also { anonymous.key(key) }
+            principal?.also { anonymous.principal(principal) }
+            authorities?.also { anonymous.authorities(authorities) }
+            authenticationFilter?.also { anonymous.authenticationFilter(authenticationFilter) }
+            if (disabled) {
+                anonymous.disable()
+            }
+        }
+    }
+}

+ 49 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.web.cors.reactive.CorsConfigurationSource
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] CORS headers using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property configurationSource the [CorsConfigurationSource] to use.
+ */
+class ServerCorsDsl {
+    var configurationSource: CorsConfigurationSource? = null
+
+    private var disabled = false
+
+    /**
+     * Disables CORS support within Spring Security.
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.CorsSpec) -> Unit {
+        return { cors ->
+            configurationSource?.also { cors.configurationSource(configurationSource) }
+            if (disabled) {
+                cors.disable()
+            }
+        }
+    }
+}

+ 59 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
+import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] CSRF protection using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property accessDeniedHandler the [ServerAccessDeniedHandler] used when a CSRF token is invalid.
+ * @property csrfTokenRepository the [ServerCsrfTokenRepository] used to persist the CSRF token.
+ * @property requireCsrfProtectionMatcher the [ServerWebExchangeMatcher] used to determine when CSRF protection
+ * is enabled.
+ */
+class ServerCsrfDsl {
+    var accessDeniedHandler: ServerAccessDeniedHandler? = null
+    var csrfTokenRepository: ServerCsrfTokenRepository? = null
+    var requireCsrfProtectionMatcher: ServerWebExchangeMatcher? = null
+
+    private var disabled = false
+
+    /**
+     * Disables CSRF protection
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.CsrfSpec) -> Unit {
+        return { csrf ->
+            accessDeniedHandler?.also { csrf.accessDeniedHandler(accessDeniedHandler) }
+            csrfTokenRepository?.also { csrf.csrfTokenRepository(csrfTokenRepository) }
+            requireCsrfProtectionMatcher?.also { csrf.requireCsrfProtectionMatcher(requireCsrfProtectionMatcher) }
+            if (disabled) {
+                csrf.disable()
+            }
+        }
+    }
+}

+ 43 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint
+import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] exception handling using idiomatic Kotlin
+ * code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use when
+ * the application request authentication
+ * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use when an
+ * authenticated user does not hold a required authority
+ */
+class ServerExceptionHandlingDsl {
+    var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
+    var accessDeniedHandler: ServerAccessDeniedHandler? = null
+
+    internal fun get(): (ServerHttpSecurity.ExceptionHandlingSpec) -> Unit {
+        return { exceptionHandling ->
+            authenticationEntryPoint?.also { exceptionHandling.authenticationEntryPoint(authenticationEntryPoint) }
+            accessDeniedHandler?.also { exceptionHandling.accessDeniedHandler(accessDeniedHandler) }
+        }
+    }
+}

+ 85 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint
+import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
+import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
+import org.springframework.security.web.server.context.ReactorContextWebFilter
+import org.springframework.security.web.server.context.ServerSecurityContextRepository
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate.
+ * @property loginPage the url to redirect to which provides a form to log in (i.e. "/login").
+ * If this is customized:
+ * - The default log in & log out page are no longer provided
+ * - The application must render a log in page at the provided URL
+ * - The application must render an authentication error page at the provided URL + "?error"
+ * - Authentication will occur for POST to the provided URL
+ * @property authenticationEntryPoint configures how to request for authentication.
+ * @property requiresAuthenticationMatcher configures when authentication is performed.
+ * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after
+ * authentication success.
+ * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used to handle
+ * a failed authentication.
+ * @property securityContextRepository the [ServerSecurityContextRepository] used to save
+ * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the
+ * [ReactorContextWebFilter] must be configured to be able to load the value (they are not
+ * implicitly linked).
+ */
+class ServerFormLoginDsl {
+    var authenticationManager: ReactiveAuthenticationManager? = null
+    var loginPage: String? = null
+    var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
+    var requiresAuthenticationMatcher: ServerWebExchangeMatcher? = null
+    var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
+    var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null
+    var securityContextRepository: ServerSecurityContextRepository? = null
+
+    private var disabled = false
+
+    /**
+     * Disables HTTP basic authentication
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.FormLoginSpec) -> Unit {
+        return { formLogin ->
+            authenticationManager?.also { formLogin.authenticationManager(authenticationManager) }
+            loginPage?.also { formLogin.loginPage(loginPage) }
+            authenticationEntryPoint?.also { formLogin.authenticationEntryPoint(authenticationEntryPoint) }
+            requiresAuthenticationMatcher?.also { formLogin.requiresAuthenticationMatcher(requiresAuthenticationMatcher) }
+            authenticationSuccessHandler?.also { formLogin.authenticationSuccessHandler(authenticationSuccessHandler) }
+            authenticationFailureHandler?.also { formLogin.authenticationFailureHandler(authenticationFailureHandler) }
+            securityContextRepository?.also { formLogin.securityContextRepository(securityContextRepository) }
+            if (disabled) {
+                formLogin.disable()
+            }
+        }
+    }
+}

+ 181 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt

@@ -0,0 +1,181 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.config.web.server.headers.*
+import org.springframework.security.web.server.header.*
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class ServerHeadersDsl {
+    private var contentTypeOptions: ((ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit)? = null
+    private var xssProtection: ((ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit)? = null
+    private var cacheControl: ((ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit)? = null
+    private var hsts: ((ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit)? = null
+    private var frameOptions: ((ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit)? = null
+    private var contentSecurityPolicy: ((ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit)? = null
+    private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null
+    private var featurePolicyDirectives: String? = null
+
+    private var disabled = false
+
+    /**
+     * Configures the [ContentTypeOptionsServerHttpHeadersWriter] which inserts the <a href=
+     * "https://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx"
+     * >X-Content-Type-Options header</a>
+     *
+     * @param contentTypeOptionsConfig the customization to apply to the header
+     */
+    fun contentTypeOptions(contentTypeOptionsConfig: ServerContentTypeOptionsDsl.() -> Unit) {
+        this.contentTypeOptions = ServerContentTypeOptionsDsl().apply(contentTypeOptionsConfig).get()
+    }
+
+    /**
+     * <strong>Note this is not comprehensive XSS protection!</strong>
+     *
+     * <p>
+     * Allows customizing the [XXssProtectionServerHttpHeadersWriter] which adds the <a href=
+     * "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
+     * >X-XSS-Protection header</a>
+     * </p>
+     *
+     * @param xssProtectionConfig the customization to apply to the header
+     */
+    fun xssProtection(xssProtectionConfig: ServerXssProtectionDsl.() -> Unit) {
+        this.xssProtection = ServerXssProtectionDsl().apply(xssProtectionConfig).get()
+    }
+
+    /**
+     * Allows customizing the [CacheControlServerHttpHeadersWriter]. Specifically it adds
+     * the following headers:
+     * <ul>
+     * <li>Cache-Control: no-cache, no-store, max-age=0, must-revalidate</li>
+     * <li>Pragma: no-cache</li>
+     * <li>Expires: 0</li>
+     * </ul>
+     *
+     * @param cacheControlConfig the customization to apply to the headers
+     */
+    fun cache(cacheControlConfig: ServerCacheControlDsl.() -> Unit) {
+        this.cacheControl = ServerCacheControlDsl().apply(cacheControlConfig).get()
+    }
+
+    /**
+     * Allows customizing the [StrictTransportSecurityServerHttpHeadersWriter] which provides support
+     * for <a href="https://tools.ietf.org/html/rfc6797">HTTP Strict Transport Security
+     * (HSTS)</a>.
+     *
+     * @param hstsConfig the customization to apply to the header
+     */
+    fun hsts(hstsConfig: ServerHttpStrictTransportSecurityDsl.() -> Unit) {
+        this.hsts = ServerHttpStrictTransportSecurityDsl().apply(hstsConfig).get()
+    }
+
+    /**
+     * Allows customizing the [XFrameOptionsServerHttpHeadersWriter] which add the X-Frame-Options
+     * header.
+     *
+     * @param frameOptionsConfig the customization to apply to the header
+     */
+    fun frameOptions(frameOptionsConfig: ServerFrameOptionsDsl.() -> Unit) {
+        this.frameOptions = ServerFrameOptionsDsl().apply(frameOptionsConfig).get()
+    }
+
+    /**
+     * Allows configuration for <a href="https://www.w3.org/TR/CSP2/">Content Security Policy (CSP) Level 2</a>.
+     *
+     * @param contentSecurityPolicyConfig the customization to apply to the header
+     */
+    fun contentSecurityPolicy(contentSecurityPolicyConfig: ServerContentSecurityPolicyDsl.() -> Unit) {
+        this.contentSecurityPolicy = ServerContentSecurityPolicyDsl().apply(contentSecurityPolicyConfig).get()
+    }
+
+    /**
+     * Allows configuration for <a href="https://www.w3.org/TR/referrer-policy/">Referrer Policy</a>.
+     *
+     * <p>
+     * Configuration is provided to the [ReferrerPolicyServerHttpHeadersWriter] which support the writing
+     * of the header as detailed in the W3C Technical Report:
+     * </p>
+     * <ul>
+     *  <li>Referrer-Policy</li>
+     * </ul>
+     *
+     * @param referrerPolicyConfig the customization to apply to the header
+     */
+    fun referrerPolicy(referrerPolicyConfig: ServerReferrerPolicyDsl.() -> Unit) {
+        this.referrerPolicy = ServerReferrerPolicyDsl().apply(referrerPolicyConfig).get()
+    }
+
+    /**
+     * Allows configuration for <a href="https://wicg.github.io/feature-policy/">Feature
+     * Policy</a>.
+     *
+     * <p>
+     * Calling this method automatically enables (includes) the Feature-Policy
+     * header in the response using the supplied policy directive(s).
+     * <p>
+     *
+     * @param policyDirectives policyDirectives the security policy directive(s)
+     */
+    fun featurePolicy(policyDirectives: String) {
+        this.featurePolicyDirectives = policyDirectives
+    }
+
+    /**
+     * Disables HTTP response headers.
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec) -> Unit {
+        return { headers ->
+            contentTypeOptions?.also {
+                headers.contentTypeOptions(contentTypeOptions)
+            }
+            xssProtection?.also {
+                headers.xssProtection(xssProtection)
+            }
+            cacheControl?.also {
+                headers.cache(cacheControl)
+            }
+            hsts?.also {
+                headers.hsts(hsts)
+            }
+            frameOptions?.also {
+                headers.frameOptions(frameOptions)
+            }
+            contentSecurityPolicy?.also {
+                headers.contentSecurityPolicy(contentSecurityPolicy)
+            }
+            featurePolicyDirectives?.also {
+                headers.featurePolicy(featurePolicyDirectives)
+            }
+            referrerPolicy?.also {
+                headers.referrerPolicy(referrerPolicy)
+            }
+            if (disabled) {
+                headers.disable()
+            }
+        }
+    }
+}

+ 65 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.context.SecurityContext
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint
+import org.springframework.security.web.server.context.ReactorContextWebFilter
+import org.springframework.security.web.server.context.ServerSecurityContextRepository
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] basic authorization using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate.
+ * @property securityContextRepository the [ServerSecurityContextRepository] used to save
+ * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the
+ * [ReactorContextWebFilter] must be configured to be able to load the value (they are not
+ * implicitly linked).
+ * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to be
+ * populated on [BasicAuthenticationFilter] in the event that authentication fails.
+ */
+class ServerHttpBasicDsl {
+    var authenticationManager: ReactiveAuthenticationManager? = null
+    var securityContextRepository: ServerSecurityContextRepository? = null
+    var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
+
+    private var disabled = false
+
+    /**
+     * Disables HTTP basic authentication
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HttpBasicSpec) -> Unit {
+        return { httpBasic ->
+            authenticationManager?.also { httpBasic.authenticationManager(authenticationManager) }
+            securityContextRepository?.also { httpBasic.securityContextRepository(securityContextRepository) }
+            authenticationEntryPoint?.also { httpBasic.authenticationEntryPoint(authenticationEntryPoint) }
+            if (disabled) {
+                httpBasic.disable()
+            }
+        }
+    }
+}

+ 528 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt

@@ -0,0 +1,528 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.web.server.ServerWebExchange
+
+/**
+ * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl].
+ *
+ * Example:
+ *
+ * ```
+ * @EnableWebFluxSecurity
+ * class SecurityConfig {
+ *
+ *  @Bean
+ *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ *      return http {
+ *          authorizeExchange {
+ *              exchange("/public", permitAll)
+ *              exchange(anyExchange, authenticated)
+ *          }
+ *       }
+ *   }
+ * }
+ * ```
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @param httpConfiguration the configurations to apply to [ServerHttpSecurity]
+ */
+operator fun ServerHttpSecurity.invoke(httpConfiguration: ServerHttpSecurityDsl.() -> Unit): SecurityWebFilterChain =
+        ServerHttpSecurityDsl(this, httpConfiguration).build()
+
+/**
+ * A [ServerHttpSecurity] Kotlin DSL created by [`http { }`][invoke]
+ * in order to configure [ServerHttpSecurity] using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @param init the configurations to apply to the provided [ServerHttpSecurity]
+ */
+class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val init: ServerHttpSecurityDsl.() -> Unit) {
+
+    /**
+     * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the
+     * provided [ServerWebExchangeMatcher].
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/&ast;&ast;"))
+     *          formLogin {
+     *              loginPage = "/log-in"
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param securityMatcher a [ServerWebExchangeMatcher] used to determine whether this
+     * configuration should be invoked.
+     */
+    fun securityMatcher(securityMatcher: ServerWebExchangeMatcher) {
+        this.http.securityMatcher(securityMatcher)
+    }
+
+    /**
+     * Enables form based authentication.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          formLogin {
+     *              loginPage = "/log-in"
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param formLoginConfiguration custom configuration to apply to the form based
+     * authentication
+     * @see [ServerFormLoginDsl]
+     */
+    fun formLogin(formLoginConfiguration: ServerFormLoginDsl.() -> Unit) {
+        val formLoginCustomizer = ServerFormLoginDsl().apply(formLoginConfiguration).get()
+        this.http.formLogin(formLoginCustomizer)
+    }
+
+    /**
+     * Allows restricting access based upon the [ServerWebExchange]
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          authorizeExchange {
+     *              exchange("/public", permitAll)
+     *              exchange(anyExchange, authenticated)
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param authorizeExchangeConfiguration custom configuration that specifies
+     * access for an exchange
+     * @see [AuthorizeExchangeDsl]
+     */
+    fun authorizeExchange(authorizeExchangeConfiguration: AuthorizeExchangeDsl.() -> Unit) {
+        val authorizeExchangeCustomizer = AuthorizeExchangeDsl().apply(authorizeExchangeConfiguration).get()
+        this.http.authorizeExchange(authorizeExchangeCustomizer)
+    }
+
+    /**
+     * Enables HTTP basic authentication.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          httpBasic { }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param httpBasicConfiguration custom configuration to be applied to the
+     * HTTP basic authentication
+     * @see [ServerHttpBasicDsl]
+     */
+    fun httpBasic(httpBasicConfiguration: ServerHttpBasicDsl.() -> Unit) {
+        val httpBasicCustomizer = ServerHttpBasicDsl().apply(httpBasicConfiguration).get()
+        this.http.httpBasic(httpBasicCustomizer)
+    }
+
+    /**
+     * Allows configuring response headers.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          headers {
+     *              referrerPolicy {
+     *                  policy = ReferrerPolicy.SAME_ORIGIN
+     *              }
+     *              frameOptions {
+     *                  mode = Mode.DENY
+     *              }
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param headersConfiguration custom configuration to be applied to the
+     * response headers
+     * @see [ServerHeadersDsl]
+     */
+    fun headers(headersConfiguration: ServerHeadersDsl.() -> Unit) {
+        val headersCustomizer = ServerHeadersDsl().apply(headersConfiguration).get()
+        this.http.headers(headersCustomizer)
+    }
+
+    /**
+     * Allows configuring CORS.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          cors {
+     *              configurationSource = customConfigurationSource
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param corsConfiguration custom configuration to be applied to the
+     * CORS headers
+     * @see [ServerCorsDsl]
+     */
+    fun cors(corsConfiguration: ServerCorsDsl.() -> Unit) {
+        val corsCustomizer = ServerCorsDsl().apply(corsConfiguration).get()
+        this.http.cors(corsCustomizer)
+    }
+
+    /**
+     * Allows configuring HTTPS redirection rules.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          redirectToHttps {
+     *              httpsRedirectWhen {
+     *                  it.request.headers.containsKey("X-Requires-Https")
+     *              }
+     *          }
+     *      }
+     *   }
+     * }
+     * ```
+     *
+     * @param httpsRedirectConfiguration custom configuration for the HTTPS redirect
+     * rules.
+     * @see [ServerHttpsRedirectDsl]
+     */
+    fun redirectToHttps(httpsRedirectConfiguration: ServerHttpsRedirectDsl.() -> Unit) {
+        val httpsRedirectCustomizer = ServerHttpsRedirectDsl().apply(httpsRedirectConfiguration).get()
+        this.http.redirectToHttps(httpsRedirectCustomizer)
+    }
+
+    /**
+     * Allows configuring exception handling.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          exceptionHandling {
+     *              authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth")
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param exceptionHandlingConfiguration custom configuration to apply to
+     * exception handling
+     * @see [ServerExceptionHandlingDsl]
+     */
+    fun exceptionHandling(exceptionHandlingConfiguration: ServerExceptionHandlingDsl.() -> Unit) {
+        val exceptionHandlingCustomizer = ServerExceptionHandlingDsl().apply(exceptionHandlingConfiguration).get()
+        this.http.exceptionHandling(exceptionHandlingCustomizer)
+    }
+
+    /**
+     * Adds X509 based pre authentication to an application using a certificate provided by a client.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          x509 { }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param x509Configuration custom configuration to apply to the X509 based pre authentication
+     * @see [ServerX509Dsl]
+     */
+    fun x509(x509Configuration: ServerX509Dsl.() -> Unit) {
+        val x509Customizer = ServerX509Dsl().apply(x509Configuration).get()
+        this.http.x509(x509Customizer)
+    }
+
+    /**
+     * Allows configuring request cache which is used when a flow is interrupted (i.e. due to requesting credentials)
+     * so that the request can be replayed after authentication.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          requestCache { }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param requestCacheConfiguration custom configuration to apply to the request cache
+     * @see [ServerRequestCacheDsl]
+     */
+    fun requestCache(requestCacheConfiguration: ServerRequestCacheDsl.() -> Unit) {
+        val requestCacheCustomizer = ServerRequestCacheDsl().apply(requestCacheConfiguration).get()
+        this.http.requestCache(requestCacheCustomizer)
+    }
+
+    /**
+     * Enables CSRF protection.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          csrf { }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param csrfConfiguration custom configuration to apply to the CSRF protection
+     * @see [ServerCsrfDsl]
+     */
+    fun csrf(csrfConfiguration: ServerCsrfDsl.() -> Unit) {
+        val csrfCustomizer = ServerCsrfDsl().apply(csrfConfiguration).get()
+        this.http.csrf(csrfCustomizer)
+    }
+
+    /**
+     * Provides logout support.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          logout {
+     *              logoutUrl = "/sign-out"
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param logoutConfiguration custom configuration to apply to logout
+     * @see [ServerLogoutDsl]
+     */
+    fun logout(logoutConfiguration: ServerLogoutDsl.() -> Unit) {
+        val logoutCustomizer = ServerLogoutDsl().apply(logoutConfiguration).get()
+        this.http.logout(logoutCustomizer)
+    }
+
+    /**
+     * Enables and configures anonymous authentication.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          anonymous {
+     *              authorities = listOf(SimpleGrantedAuthority("ROLE_ANON"))
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param anonymousConfiguration custom configuration to apply to anonymous authentication
+     * @see [ServerAnonymousDsl]
+     */
+    fun anonymous(anonymousConfiguration: ServerAnonymousDsl.() -> Unit) {
+        val anonymousCustomizer = ServerAnonymousDsl().apply(anonymousConfiguration).get()
+        this.http.anonymous(anonymousCustomizer)
+    }
+
+    /**
+     * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider.
+     * A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or
+     * configured via [ServerOAuth2LoginDsl.clientRegistrationRepository].
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          oauth2Login {
+     *              clientRegistrationRepository = getClientRegistrationRepository()
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param oauth2LoginConfiguration custom configuration to configure the OAuth 2.0 Login
+     * @see [ServerOAuth2LoginDsl]
+     */
+    fun oauth2Login(oauth2LoginConfiguration: ServerOAuth2LoginDsl.() -> Unit) {
+        val oauth2LoginCustomizer = ServerOAuth2LoginDsl().apply(oauth2LoginConfiguration).get()
+        this.http.oauth2Login(oauth2LoginCustomizer)
+    }
+
+    /**
+     * Configures OAuth2 client support.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          oauth2Client {
+     *              clientRegistrationRepository = getClientRegistrationRepository()
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param oauth2ClientConfiguration custom configuration to configure the OAuth 2.0 client
+     * @see [ServerOAuth2ClientDsl]
+     */
+    fun oauth2Client(oauth2ClientConfiguration: ServerOAuth2ClientDsl.() -> Unit) {
+        val oauth2ClientCustomizer = ServerOAuth2ClientDsl().apply(oauth2ClientConfiguration).get()
+        this.http.oauth2Client(oauth2ClientCustomizer)
+    }
+
+    /**
+     * Configures OAuth2 resource server support.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          oauth2ResourceServer {
+     *              jwt { }
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param oauth2ResourceServerConfiguration custom configuration to configure the OAuth 2.0 resource server
+     * @see [ServerOAuth2ResourceServerDsl]
+     */
+    fun oauth2ResourceServer(oauth2ResourceServerConfiguration: ServerOAuth2ResourceServerDsl.() -> Unit) {
+        val oauth2ResourceServerCustomizer = ServerOAuth2ResourceServerDsl().apply(oauth2ResourceServerConfiguration).get()
+        this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
+    }
+
+    /**
+     * Apply all configurations to the provided [ServerHttpSecurity]
+     */
+    internal fun build(): SecurityWebFilterChain {
+        init()
+        return this.http.build()
+    }
+}

+ 70 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.web.PortMapper
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.web.server.ServerWebExchange
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] HTTPS redirection rules using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property portMapper the [PortMapper] that specifies a custom HTTPS port to redirect to.
+ */
+class ServerHttpsRedirectDsl {
+    var portMapper: PortMapper? = null
+
+    private var redirectMatchers: Array<out ServerWebExchangeMatcher>? = null
+    private var redirectMatcherFunction: ((ServerWebExchange) -> Boolean)? = null
+
+    /**
+     * Configures when this filter should redirect to https.
+     * If invoked multiple times, whether a matcher or a function is provided, only the
+     * last redirect rule will apply and all previous rules will be overridden.
+     *
+     * @param redirectMatchers the list of conditions that, when any are met, the
+     * filter should redirect to https.
+     */
+    fun httpsRedirectWhen(vararg redirectMatchers: ServerWebExchangeMatcher) {
+        this.redirectMatcherFunction = null
+        this.redirectMatchers = redirectMatchers
+    }
+
+    /**
+     * Configures when this filter should redirect to https.
+     * If invoked multiple times, whether a matcher or a function is provided, only the
+     * last redirect rule will apply and all previous rules will be overridden.
+     *
+     * @param redirectMatcherFunction the condition in which the filter should redirect to
+     * https.
+     */
+    fun httpsRedirectWhen(redirectMatcherFunction: (ServerWebExchange) -> Boolean) {
+        this.redirectMatchers = null
+        this.redirectMatcherFunction = redirectMatcherFunction
+    }
+
+    internal fun get(): (ServerHttpSecurity.HttpsRedirectSpec) -> Unit {
+        return { httpsRedirect ->
+            portMapper?.also { httpsRedirect.portMapper(portMapper) }
+            redirectMatchers?.also { httpsRedirect.httpsRedirectWhen(*redirectMatchers!!) }
+            redirectMatcherFunction?.also { httpsRedirect.httpsRedirectWhen(redirectMatcherFunction) }
+        }
+    }
+}

+ 61 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] logout support using idiomatic Kotlin
+ * code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property logoutHandler a [ServerLogoutHandler] that is invoked when logout occurs.
+ * @property logoutUrl the URL that triggers logout to occur.
+ * @property requiresLogout the [ServerWebExchangeMatcher] that triggers logout to occur.
+ * @property logoutSuccessHandler the [ServerLogoutSuccessHandler] to use after logout has
+ * occurred.
+ */
+class ServerLogoutDsl {
+    var logoutHandler: ServerLogoutHandler? = null
+    var logoutUrl: String? = null
+    var requiresLogout: ServerWebExchangeMatcher? = null
+    var logoutSuccessHandler: ServerLogoutSuccessHandler? = null
+
+    private var disabled = false
+
+    /**
+     * Disables logout
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.LogoutSpec) -> Unit {
+        return { logout ->
+            logoutHandler?.also { logout.logoutHandler(logoutHandler) }
+            logoutUrl?.also { logout.logoutUrl(logoutUrl) }
+            requiresLogout?.also { logout.requiresLogout(requiresLogout) }
+            logoutSuccessHandler?.also { logout.logoutSuccessHandler(logoutSuccessHandler) }
+            if (disabled) {
+                logout.disable()
+            }
+        }
+    }
+}

+ 57 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
+import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
+import org.springframework.web.server.ServerWebExchange
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] OAuth 2.0 client using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided
+ * [Authentication] can be authenticated.
+ * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange]
+ * to an [Authentication].
+ * @property clientRegistrationRepository the repository of client registrations.
+ * @property authorizedClientRepository the repository for authorized client(s).
+ * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s.
+ */
+class ServerOAuth2ClientDsl {
+    var authenticationManager: ReactiveAuthenticationManager? = null
+    var authenticationConverter: ServerAuthenticationConverter? = null
+    var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null
+    var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null
+    var authorizationRequestRepository: ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>? = null
+
+    internal fun get(): (ServerHttpSecurity.OAuth2ClientSpec) -> Unit {
+        return { oauth2Client ->
+            authenticationManager?.also { oauth2Client.authenticationManager(authenticationManager) }
+            authenticationConverter?.also { oauth2Client.authenticationConverter(authenticationConverter) }
+            clientRegistrationRepository?.also { oauth2Client.clientRegistrationRepository(clientRegistrationRepository) }
+            authorizedClientRepository?.also { oauth2Client.authorizedClientRepository(authorizedClientRepository) }
+            authorizationRequestRepository?.also { oauth2Client.authorizationRequestRepository(authorizationRequestRepository) }
+        }
+    }
+}

+ 83 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
+import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver
+import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
+import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
+import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
+import org.springframework.security.web.server.context.ServerSecurityContextRepository
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.web.server.ServerWebExchange
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 login using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided
+ * [Authentication] can be authenticated.
+ * @property securityContextRepository the [ServerSecurityContextRepository] used to save the [Authentication].
+ * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after authentication success.
+ * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used after authentication failure.
+ * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange]
+ * to an [Authentication].
+ * @property clientRegistrationRepository the repository of client registrations.
+ * @property authorizedClientService the service responsible for associating an access token to a client and resource
+ * owner.
+ * @property authorizedClientRepository the repository for authorized client(s).
+ * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s.
+ * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]s.
+ * @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an
+ * authentication request.
+ */
+class ServerOAuth2LoginDsl {
+    var authenticationManager: ReactiveAuthenticationManager? = null
+    var securityContextRepository: ServerSecurityContextRepository? = null
+    var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
+    var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null
+    var authenticationConverter: ServerAuthenticationConverter? = null
+    var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null
+    var authorizedClientService: ReactiveOAuth2AuthorizedClientService? = null
+    var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null
+    var authorizationRequestRepository: ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>? = null
+    var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null
+    var authenticationMatcher: ServerWebExchangeMatcher? = null
+
+    internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit {
+        return { oauth2Login ->
+            authenticationManager?.also { oauth2Login.authenticationManager(authenticationManager) }
+            securityContextRepository?.also { oauth2Login.securityContextRepository(securityContextRepository) }
+            authenticationSuccessHandler?.also { oauth2Login.authenticationSuccessHandler(authenticationSuccessHandler) }
+            authenticationFailureHandler?.also { oauth2Login.authenticationFailureHandler(authenticationFailureHandler) }
+            authenticationConverter?.also { oauth2Login.authenticationConverter(authenticationConverter) }
+            clientRegistrationRepository?.also { oauth2Login.clientRegistrationRepository(clientRegistrationRepository) }
+            authorizedClientService?.also { oauth2Login.authorizedClientService(authorizedClientService) }
+            authorizedClientRepository?.also { oauth2Login.authorizedClientRepository(authorizedClientRepository) }
+            authorizationRequestRepository?.also { oauth2Login.authorizationRequestRepository(authorizationRequestRepository) }
+            authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) }
+            authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) }
+        }
+    }
+}

+ 118 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver
+import org.springframework.security.config.web.server.oauth2.resourceserver.ServerJwtDsl
+import org.springframework.security.config.web.server.oauth2.resourceserver.ServerOpaqueTokenDsl
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
+import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
+import org.springframework.web.server.ServerWebExchange
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 resource server using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use for requests authenticating with
+ * Bearer Tokens.
+ * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use for requests authenticating with
+ * Bearer Tokens.
+ * @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with
+ * Bearer Tokens.
+ * @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use.
+ */
+class ServerOAuth2ResourceServerDsl {
+    var accessDeniedHandler: ServerAccessDeniedHandler? = null
+    var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
+    var bearerTokenConverter: ServerAuthenticationConverter? = null
+    var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null
+
+    private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null
+    private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null
+
+    /**
+     * Enables JWT-encoded bearer token support.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          oauth2ResourceServer {
+     *              jwt {
+     *                  jwkSetUri = "https://example.com/oauth2/jwk"
+     *              }
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param jwtConfig custom configurations to configure JWT resource server support
+     * @see [ServerJwtDsl]
+     */
+    fun jwt(jwtConfig: ServerJwtDsl.() -> Unit) {
+        this.jwt = ServerJwtDsl().apply(jwtConfig).get()
+    }
+
+    /**
+     * Enables opaque token support.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          oauth2ResourceServer {
+     *              opaqueToken {
+     *                  introspectionUri = "https://example.com/introspect"
+     *                  introspectionClientCredentials("client", "secret")
+     *              }
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param opaqueTokenConfig custom configurations to configure JWT resource server support
+     * @see [ServerOpaqueTokenDsl]
+     */
+    fun opaqueToken(opaqueTokenConfig: ServerOpaqueTokenDsl.() -> Unit) {
+        this.opaqueToken = ServerOpaqueTokenDsl().apply(opaqueTokenConfig).get()
+    }
+
+    internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec) -> Unit {
+        return { oauth2ResourceServer ->
+            accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) }
+            authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) }
+            bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) }
+            authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) }
+            jwt?.also { oauth2ResourceServer.jwt(jwt) }
+            opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
+        }
+    }
+}

+ 50 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.web.server.savedrequest.ServerRequestCache
+
+/**
+ * A Kotlin DSL to configure the request cache using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property requestCache allows explicit configuration of the [ServerRequestCache] to be used.
+ */
+class ServerRequestCacheDsl {
+    var requestCache: ServerRequestCache? = null
+
+    private var disabled = false
+
+    /**
+     * Disables the request cache.
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.RequestCacheSpec) -> Unit {
+        return { requestCacheConfig ->
+            requestCache?.also {
+                requestCacheConfig.requestCache(requestCache)
+                if (disabled) {
+                    requestCacheConfig.disable()
+                }
+            }
+        }
+    }
+}

+ 42 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.core.Authentication
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] X509 based pre authentication using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property principalExtractor the [X509PrincipalExtractor] used to obtain the principal for use within the framework.
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided
+ * [Authentication] can be authenticated.
+ */
+class ServerX509Dsl {
+    var principalExtractor: X509PrincipalExtractor? = null
+    var authenticationManager: ReactiveAuthenticationManager? = null
+
+    internal fun get(): (ServerHttpSecurity.X509Spec) -> Unit {
+        return { x509 ->
+            authenticationManager?.also { x509.authenticationManager(authenticationManager) }
+            principalExtractor?.also { x509.principalExtractor(principalExtractor) }
+        }
+    }
+}

+ 45 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDsl.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] cache control headers using
+ * idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class ServerCacheControlDsl {
+    private var disabled = false
+
+    /**
+     * Disables cache control response headers
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit {
+        return { cacheControl ->
+            if (disabled) {
+                cacheControl.disable()
+            }
+        }
+    }
+}

+ 42 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDsl.kt

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] Content-Security-Policy header using
+ * idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class ServerContentSecurityPolicyDsl {
+    var policyDirectives: String? = null
+    var reportOnly: Boolean? = null
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit {
+        return { contentSecurityPolicy ->
+            policyDirectives?.also {
+                contentSecurityPolicy.policyDirectives(policyDirectives)
+            }
+            reportOnly?.also {
+                contentSecurityPolicy.reportOnly(reportOnly!!)
+            }
+        }
+    }
+}

+ 45 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDsl.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] the content type options header
+ * using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class ServerContentTypeOptionsDsl {
+    private var disabled = false
+
+    /**
+     * Disables content type options response header
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit {
+        return { contentTypeOptions ->
+            if (disabled) {
+                contentTypeOptions.disable()
+            }
+        }
+    }
+}

+ 52 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDsl.kt

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] X-Frame-Options header using
+ * idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property mode the X-Frame-Options mode to set in the response header.
+ */
+class ServerFrameOptionsDsl {
+    var mode: XFrameOptionsServerHttpHeadersWriter.Mode? = null
+
+    private var disabled = false
+
+    /**
+     * Disables the X-Frame-Options response header
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit {
+        return { frameOptions ->
+            mode?.also {
+                frameOptions.mode(mode)
+            }
+            if (disabled) {
+                frameOptions.disable()
+            }
+        }
+    }
+}

+ 57 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDsl.kt

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import java.time.Duration
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] HTTP Strict Transport Security
+ * header using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property maxAge he value for the max-age directive of the Strict-Transport-Security
+ * header.
+ * @property includeSubdomains if true, subdomains should be considered HSTS Hosts too.
+ * @property preload if true, preload will be included in HSTS Header.
+ */
+class ServerHttpStrictTransportSecurityDsl {
+    var maxAge: Duration? = null
+    var includeSubdomains: Boolean? = null
+    var preload: Boolean? = null
+
+    private var disabled = false
+
+    /**
+     * Disables the X-Frame-Options response header
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit {
+        return { hsts ->
+            maxAge?.also { hsts.maxAge(maxAge) }
+            includeSubdomains?.also { hsts.includeSubdomains(includeSubdomains!!) }
+            preload?.also { hsts.preload(preload!!) }
+            if (disabled) {
+                hsts.disable()
+            }
+        }
+    }
+}

+ 40 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDsl.kt

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] referrer policy header using
+ * idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property policy the policy to be used in the response header.
+ */
+class ServerReferrerPolicyDsl {
+    var policy: ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy? = null
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit {
+        return { referrerPolicy ->
+            policy?.also {
+                referrerPolicy.policy(policy)
+            }
+        }
+    }
+}

+ 45 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDsl.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+
+/**
+ * A Kotlin DSL to configure the [ServerHttpSecurity] XSS protection header using
+ * idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+class ServerXssProtectionDsl {
+    private var disabled = false
+
+    /**
+     * Disables cache control response headers
+     */
+    fun disable() {
+        disabled = true
+    }
+
+    internal fun get(): (ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit {
+        return { xss ->
+            if (disabled) {
+                xss.disable()
+            }
+        }
+    }
+}

+ 82 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDsl.kt

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2002-2020 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.web.server.oauth2.resourceserver
+
+import org.springframework.core.convert.converter.Converter
+import org.springframework.security.authentication.AbstractAuthenticationToken
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.core.Authentication
+import org.springframework.security.oauth2.jwt.Jwt
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
+import reactor.core.publisher.Mono
+import java.security.interfaces.RSAPublicKey
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] JWT Resource Server support using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided
+ * [Authentication] can be authenticated.
+ * @property jwtAuthenticationConverter the [Converter] to use for converting a [Jwt] into an
+ * [AbstractAuthenticationToken].
+ * @property jwtDecoder the [ReactiveJwtDecoder] to use.
+ * @property publicKey configures a [ReactiveJwtDecoder] that leverages the provided [RSAPublicKey]
+ * @property jwkSetUri configures a [ReactiveJwtDecoder] using a
+ * <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a> URL
+ */
+class ServerJwtDsl {
+    private var _jwtDecoder: ReactiveJwtDecoder? = null
+    private var _publicKey: RSAPublicKey? = null
+    private var _jwkSetUri: String? = null
+
+    var authenticationManager: ReactiveAuthenticationManager? = null
+    var jwtAuthenticationConverter: Converter<Jwt, out Mono<out AbstractAuthenticationToken>>? = null
+
+    var jwtDecoder: ReactiveJwtDecoder?
+        get() = _jwtDecoder
+        set(value) {
+            _jwtDecoder = value
+            _publicKey = null
+            _jwkSetUri = null
+        }
+    var publicKey: RSAPublicKey?
+        get() = _publicKey
+        set(value) {
+            _publicKey = value
+            _jwtDecoder = null
+            _jwkSetUri = null
+        }
+    var jwkSetUri: String?
+        get() = _jwkSetUri
+        set(value) {
+            _jwkSetUri = value
+            _jwtDecoder = null
+            _publicKey = null
+        }
+
+    internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit {
+        return { jwt ->
+            authenticationManager?.also { jwt.authenticationManager(authenticationManager) }
+            jwtAuthenticationConverter?.also { jwt.jwtAuthenticationConverter(jwtAuthenticationConverter) }
+            jwtDecoder?.also { jwt.jwtDecoder(jwtDecoder) }
+            publicKey?.also { jwt.publicKey(publicKey) }
+            jwkSetUri?.also { jwt.jwkSetUri(jwkSetUri) }
+        }
+    }
+}

+ 67 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDsl.kt

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2020 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.web.server.oauth2.resourceserver
+
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] Opaque Token Resource Server support using idiomatic Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property introspectionUri the URI of the Introspection endpoint.
+ * @property introspector the [ReactiveOpaqueTokenIntrospector] to use.
+ */
+class ServerOpaqueTokenDsl {
+    private var _introspectionUri: String? = null
+    private var _introspector: ReactiveOpaqueTokenIntrospector? = null
+    private var clientCredentials: Pair<String, String>? = null
+
+    var introspectionUri: String?
+        get() = _introspectionUri
+        set(value) {
+            _introspectionUri = value
+            _introspector = null
+        }
+    var introspector: ReactiveOpaqueTokenIntrospector?
+        get() = _introspector
+        set(value) {
+            _introspector = value
+            _introspectionUri = null
+            clientCredentials = null
+        }
+
+    /**
+     * Configures the credentials for Introspection endpoint.
+     *
+     * @param clientId the clientId part of the credentials.
+     * @param clientSecret the clientSecret part of the credentials.
+     */
+    fun introspectionClientCredentials(clientId: String, clientSecret: String) {
+        clientCredentials = Pair(clientId, clientSecret)
+        _introspector = null
+    }
+
+    internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit {
+        return { opaqueToken ->
+            introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
+            clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
+            introspector?.also { opaqueToken.introspector(introspector) }
+        }
+    }
+}

+ 183 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt

@@ -0,0 +1,183 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+import java.util.*
+
+/**
+ * Tests for [AuthorizeExchangeDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class AuthorizeExchangeDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when secured by matcher then responds with unauthorized`() {
+        this.spring.register(MatcherAuthenticatedConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isUnauthorized
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class MatcherAuthenticatedConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when allowed by matcher then responds with ok`() {
+        this.spring.register(MatcherPermitAllConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class MatcherPermitAllConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, permitAll)
+                }
+            }
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/")
+            fun path() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when secured by pattern then responds with unauthorized`() {
+        this.spring.register(PatternAuthenticatedConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isUnauthorized
+    }
+
+    @Test
+    fun `request when allowed by pattern then responds with ok`() {
+        this.spring.register(PatternAuthenticatedConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/public")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PatternAuthenticatedConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize("/public", permitAll)
+                    authorize("/**", authenticated)
+                }
+            }
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/public")
+            fun public() {
+            }
+        }
+    }
+
+    @Test
+    fun `request when missing required role then responds with forbidden`() {
+        this.spring.register(HasRoleConfig::class.java).autowire()
+        this.client
+                .get()
+                .uri("/")
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
+                .exchange()
+                .expectStatus().isForbidden
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HasRoleConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, hasRole("ADMIN"))
+                }
+                httpBasic { }
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("user")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+}

+ 192 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt

@@ -0,0 +1,192 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.authentication.AnonymousAuthenticationToken
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.annotation.AuthenticationPrincipal
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.reactive.server.expectBody
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+
+/**
+ * Tests for [ServerAnonymousDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerAnonymousDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `authentication when anonymous enabled then is of type anonymous authentication`() {
+        this.spring.register(AnonymousConfig::class.java, HttpMeController::class.java).autowire()
+
+        this.client.get()
+                .uri("/principal")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("anonymousUser")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AnonymousConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                anonymous { }
+            }
+        }
+    }
+
+    @Test
+    fun `anonymous when custom principal specified then custom principal is used`() {
+        this.spring.register(CustomPrincipalConfig::class.java, HttpMeController::class.java).autowire()
+
+        this.client.get()
+                .uri("/principal")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("anon")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomPrincipalConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                anonymous {
+                    principal = "anon"
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `anonymous when disabled then principal is null`() {
+        this.spring.register(AnonymousDisabledConfig::class.java, HttpMeController::class.java).autowire()
+
+        this.client.get()
+                .uri("/principal")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().consumeWith { body -> assertThat(body.responseBody).isNull() }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AnonymousDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                anonymous {
+                    disable()
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `anonymous when custom key specified then custom key used`() {
+        this.spring.register(CustomKeyConfig::class.java, HttpMeController::class.java).autowire()
+
+        this.client.get()
+                .uri("/key")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("key".hashCode().toString())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomKeyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                anonymous {
+                    key = "key"
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `anonymous when custom authorities specified then custom authorities used`() {
+        this.spring.register(CustomAuthoritiesConfig::class.java, HttpMeController::class.java).autowire()
+
+        this.client.get()
+                .uri("/principal")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomAuthoritiesConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                anonymous {
+                    authorities = listOf(SimpleGrantedAuthority("TEST"))
+                }
+                authorizeExchange {
+                    authorize(anyExchange, hasAuthority("TEST"))
+                }
+            }
+        }
+    }
+
+    @RestController
+    class HttpMeController {
+        @GetMapping("/principal")
+        fun principal(@AuthenticationPrincipal principal: String?): String? {
+            return principal
+        }
+
+        @GetMapping("/key")
+        fun key(@AuthenticationPrincipal principal: Mono<AnonymousAuthenticationToken>): Mono<String> {
+            return principal
+                    .map { it.keyHash }
+                    .map { it.toString() }
+        }
+    }
+}

+ 136 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt

@@ -0,0 +1,136 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsConfigurationSource
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerCorsDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerCorsDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when CORS configured using bean then Access-Control-Allow-Origin header in response`() {
+        this.spring.register(CorsBeanConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .header(HttpHeaders.ORIGIN, "https://origin.example.com")
+                .exchange()
+                .expectHeader().valueEquals("Access-Control-Allow-Origin", "*")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CorsBeanConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                cors { }
+            }
+        }
+
+        @Bean
+        open fun corsConfigurationSource(): CorsConfigurationSource {
+            val source = UrlBasedCorsConfigurationSource()
+            val corsConfiguration = CorsConfiguration()
+            corsConfiguration.allowedOrigins = listOf("*")
+            source.registerCorsConfiguration("/**", corsConfiguration)
+            return source
+        }
+    }
+
+    @Test
+    fun `request when CORS configured using source then Access-Control-Allow-Origin header in response`() {
+        this.spring.register(CorsSourceConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .header(HttpHeaders.ORIGIN, "https://origin.example.com")
+                .exchange()
+                .expectHeader().valueEquals("Access-Control-Allow-Origin", "*")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CorsSourceConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            val source = UrlBasedCorsConfigurationSource()
+            val corsConfiguration = CorsConfiguration()
+            corsConfiguration.allowedOrigins = listOf("*")
+            source.registerCorsConfiguration("/**", corsConfiguration)
+            return http {
+                cors {
+                    configurationSource = source
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when CORS disabled then no Access-Control-Allow-Origin header in response`() {
+        this.spring.register(CorsDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .header(HttpHeaders.ORIGIN, "https://origin.example.com")
+                .exchange()
+                .expectHeader().doesNotExist("Access-Control-Allow-Origin")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CorsDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                cors {
+                    disable()
+                }
+            }
+        }
+    }
+}

+ 210 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt

@@ -0,0 +1,210 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
+import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerCsrfDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerCsrfDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `post when CSRF protection enabled then requires CSRF token`() {
+        this.spring.register(CsrfConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+                .expectStatus().isForbidden
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CsrfConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                csrf { }
+            }
+        }
+    }
+
+    @Test
+    fun `post when CSRF protection disabled then CSRF token is not required`() {
+        this.spring.register(CsrfDisabledConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CsrfDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                csrf {
+                    disable()
+                }
+            }
+        }
+
+        @RestController
+        internal class TestController {
+            @PostMapping("/")
+            fun home() {
+            }
+        }
+    }
+
+    @Test
+    fun `post when request matches CSRF matcher then CSRF token required`() {
+        this.spring.register(CsrfMatcherConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/csrf")
+                .exchange()
+                .expectStatus().isForbidden
+    }
+
+    @Test
+    fun `post when request does not match CSRF matcher then CSRF token is not required`() {
+        this.spring.register(CsrfMatcherConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CsrfMatcherConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                csrf {
+                    requireCsrfProtectionMatcher = PathPatternParserServerWebExchangeMatcher("/csrf")
+                }
+            }
+        }
+
+        @RestController
+        internal class TestController {
+            @PostMapping("/")
+            fun home() {
+            }
+
+            @PostMapping("/csrf")
+            fun csrf() {
+            }
+        }
+    }
+
+    @Test
+    fun `csrf when custom access denied handler then handler used`() {
+        this.spring.register(CustomAccessDeniedHandlerConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+
+        Mockito.verify<ServerAccessDeniedHandler>(CustomAccessDeniedHandlerConfig.ACCESS_DENIED_HANDLER)
+                .handle(any(), any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomAccessDeniedHandlerConfig {
+        companion object {
+            var ACCESS_DENIED_HANDLER: ServerAccessDeniedHandler = mock(ServerAccessDeniedHandler::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                csrf {
+                    accessDeniedHandler = ACCESS_DENIED_HANDLER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `csrf when custom token repository then repository used`() {
+        this.spring.register(CustomCsrfTokenRepositoryConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+
+        Mockito.verify<ServerCsrfTokenRepository>(CustomCsrfTokenRepositoryConfig.TOKEN_REPOSITORY)
+                .loadToken(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomCsrfTokenRepositoryConfig {
+        companion object {
+            var TOKEN_REPOSITORY: ServerCsrfTokenRepository = mock(ServerCsrfTokenRepository::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                csrf {
+                    csrfTokenRepository = TOKEN_REPOSITORY
+                }
+            }
+        }
+    }
+}

+ 126 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.assertj.core.api.Assertions
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpStatus
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint
+import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import java.util.*
+
+/**
+ * Tests for [ServerExceptionHandlingDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerExceptionHandlingDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `unauthenticated request when custom entry point then directed to custom entry point`() {
+        this.spring.register(EntryPointConfig::class.java).autowire()
+
+        val result = this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            Assertions.assertThat(result.responseHeaders.location).hasPath("/auth")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class EntryPointConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                exceptionHandling {
+                    authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `unauthorized request when custom access denied handler then directed to custom access denied handler`() {
+        this.spring.register(AccessDeniedHandlerConfig::class.java).autowire()
+
+        this.client
+                .get()
+                .uri("/")
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
+                .exchange()
+                .expectStatus().isSeeOther
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AccessDeniedHandlerConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, hasRole("ADMIN"))
+                }
+                httpBasic { }
+                exceptionHandling {
+                    accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER)
+                }
+            }
+        }
+
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("user")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+}

+ 325 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt

@@ -0,0 +1,325 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpMethod
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
+import org.springframework.security.web.server.context.ServerSecurityContextRepository
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
+import org.springframework.test.web.reactive.server.FluxExchangeResult
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.util.LinkedMultiValueMap
+import org.springframework.util.MultiValueMap
+import org.springframework.web.reactive.config.EnableWebFlux
+import org.springframework.web.reactive.function.BodyInserters
+
+/**
+ * Tests for [ServerFormLoginDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerFormLoginDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when form login enabled then redirects to default login page`() {
+        this.spring.register(DefaultFormLoginConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        val result: FluxExchangeResult<String> = this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/login")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class DefaultFormLoginConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin { }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom login page then redirects to custom login page`() {
+        this.spring.register(CustomLoginPageConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        val result: FluxExchangeResult<String> = this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/log-in")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomLoginPageConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin {
+                    loginPage = "/log-in"
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `form login when custom authentication manager then manager used`() {
+        this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire()
+        val data: MultiValueMap<String, String> = LinkedMultiValueMap()
+        data.add("username", "user")
+        data.add("password", "password")
+
+        this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/login")
+                .body(BodyInserters.fromFormData(data))
+                .exchange()
+
+        verify<ReactiveAuthenticationManager>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER)
+                .authenticate(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomAuthenticationManagerConfig {
+        companion object {
+            var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = Mockito.mock(ReactiveAuthenticationManager::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin {
+                    authenticationManager = AUTHENTICATION_MANAGER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `form login when custom authentication entry point then entry point used`() {
+        this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        val result = this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/entry")
+        }
+    }
+
+    @Test
+    fun `form login when custom requires authentication matcher then matching request logs in`() {
+        this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
+        val data: MultiValueMap<String, String> = LinkedMultiValueMap()
+        data.add("username", "user")
+        data.add("password", "password")
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/log-in")
+                .body(BodyInserters.fromFormData(data))
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/")
+        }
+    }
+
+    @Test
+    fun `invalid login when custom failure handler then failure handler used`() {
+        this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/log-in")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/log-in-error")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin {
+                    authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/entry")
+                    requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/log-in")
+                    authenticationFailureHandler = RedirectServerAuthenticationFailureHandler("/log-in-error")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `login when custom success handler then success handler used`() {
+        this.spring.register(CustomSuccessHandlerConfig::class.java, UserDetailsConfig::class.java).autowire()
+        val data: MultiValueMap<String, String> = LinkedMultiValueMap()
+        data.add("username", "user")
+        data.add("password", "password")
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/login")
+                .body(BodyInserters.fromFormData(data))
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasPath("/success")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomSuccessHandlerConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin {
+                    authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/success")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `form login when custom security context repository then repository used`() {
+        this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire()
+        val data: MultiValueMap<String, String> = LinkedMultiValueMap()
+        data.add("username", "user")
+        data.add("password", "password")
+
+        this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/login")
+                .body(BodyInserters.fromFormData(data))
+                .exchange()
+
+        verify<ServerSecurityContextRepository>(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY)
+                .save(Mockito.any(), Mockito.any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomSecurityContextRepositoryConfig {
+        companion object {
+            var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = Mockito.mock(ServerSecurityContextRepository::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin {
+                    securityContextRepository = SECURITY_CONTEXT_REPOSITORY
+                }
+            }
+        }
+    }
+
+    @Configuration
+    open class UserDetailsConfig {
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("user")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+}

+ 134 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt

@@ -0,0 +1,134 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerHeadersDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerHeadersDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when default headers configured then default headers are in the response`() {
+        this.spring.register(DefaultHeadersConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
+                .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
+                .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
+                .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
+                .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
+                .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class DefaultHeadersConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers { }
+            }
+        }
+    }
+
+    @Test
+    fun `request when headers disabled then no security headers are in the response`() {
+        this.spring.register(HeadersDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS)
+                .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS)
+                .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY)
+                .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL)
+                .expectHeader().doesNotExist(HttpHeaders.EXPIRES)
+                .expectHeader().doesNotExist(HttpHeaders.PRAGMA)
+                .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HeadersDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    disable()
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when feature policy configured then feature policy header in response`() {
+        this.spring.register(FeaturePolicyConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals("Feature-Policy", "geolocation 'self'")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class FeaturePolicyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    featurePolicy("geolocation 'self'")
+                }
+            }
+        }
+    }
+}

+ 219 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt

@@ -0,0 +1,219 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.BDDMockito.given
+import org.mockito.Mockito.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint
+import org.springframework.security.web.server.context.ServerSecurityContextRepository
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+import java.util.*
+
+/**
+ * Tests for [ServerHttpBasicDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerHttpBasicDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `http basic when no authorization header then responds with unauthorized`() {
+        this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isUnauthorized
+    }
+
+    @Test
+    fun `http basic when valid authorization header then responds with ok`() {
+        this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HttpBasicConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                httpBasic { }
+            }
+        }
+
+        @RestController
+        internal class PathController {
+            @RequestMapping("/")
+            fun path() {
+            }
+        }
+    }
+
+    @Test
+    fun `http basic when custom authentication manager then manager used`() {
+        given<Mono<Authentication>>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any()))
+                .willReturn(Mono.just<Authentication>(TestingAuthenticationToken("user", "password", "ROLE_USER")))
+
+        this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
+                .exchange()
+
+        verify<ReactiveAuthenticationManager>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER)
+                .authenticate(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomAuthenticationManagerConfig {
+        companion object {
+            var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                httpBasic {
+                    authenticationManager = AUTHENTICATION_MANAGER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `http basic when custom security context repository then repository used`() {
+        this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
+                .exchange()
+
+        verify<ServerSecurityContextRepository>(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY)
+                .save(any(), any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomSecurityContextRepositoryConfig {
+        companion object {
+            var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = mock(ServerSecurityContextRepository::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                httpBasic {
+                    securityContextRepository = SECURITY_CONTEXT_REPOSITORY
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `http basic when custom authentication entry point then entry point used`() {
+        this.spring.register(CustomAuthenticationEntryPointConfig::class.java, UserDetailsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+
+        verify<ServerAuthenticationEntryPoint>(CustomAuthenticationEntryPointConfig.ENTRY_POINT)
+                .commence(any(), any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomAuthenticationEntryPointConfig {
+        companion object {
+            var ENTRY_POINT: ServerAuthenticationEntryPoint = mock(ServerAuthenticationEntryPoint::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                httpBasic {
+                    authenticationEntryPoint = ENTRY_POINT
+                }
+            }
+        }
+    }
+
+    @Configuration
+    open class UserDetailsConfig {
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("user")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+}

+ 126 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
+import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerHttpSecurityDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerHttpSecurityDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when it does not match the security matcher then the security rules do not apply`() {
+        this.spring.register(PatternMatcherConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isNotFound
+    }
+
+    @Test
+    fun `request when it matches the security matcher then the security rules apply`() {
+        this.spring.register(PatternMatcherConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/api")
+                .exchange()
+                .expectStatus().isUnauthorized
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PatternMatcherConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**"))
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `post when default security configured then CSRF prevents the request`() {
+        this.spring.register(DefaultSecurityConfig::class.java).autowire()
+
+        this.client.post()
+                .uri("/")
+                .exchange()
+                .expectStatus().isForbidden
+    }
+
+    @Test
+    fun `request when default security configured then default headers are in the response`() {
+        this.spring.register(DefaultSecurityConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
+                .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
+                .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
+                .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
+                .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
+                .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class DefaultSecurityConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+            }
+        }
+    }
+}

+ 199 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt

@@ -0,0 +1,199 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.PortMapperImpl
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import java.util.*
+
+/**
+ * Tests for [ServerHttpsRedirectDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerHttpsRedirectDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() {
+        this.spring.register(HttpRedirectMatcherConfig::class.java).autowire()
+
+        val result = this.client.get()
+                .uri("/secure")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasScheme("https")
+        }
+    }
+
+    @Test
+    fun `request when does not match redirect to HTTPS matcher then does not redirect`() {
+        this.spring.register(HttpRedirectMatcherConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isNotFound
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HttpRedirectMatcherConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                redirectToHttps {
+                    httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure"))
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when matches redirect to HTTPS function then redirects to HTTPS`() {
+        this.spring.register(HttpRedirectFunctionConfig::class.java).autowire()
+
+        val result = this.client.get()
+                .uri("/")
+                .header("X-Requires-Https", "required")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasScheme("https")
+        }
+    }
+
+    @Test
+    fun `request when does not match redirect to HTTPS function then does not redirect`() {
+        this.spring.register(HttpRedirectFunctionConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isNotFound
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HttpRedirectFunctionConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                redirectToHttps {
+                    httpsRedirectWhen {
+                        it.request.headers.containsKey("X-Requires-Https")
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when multiple rules configured then only the last rule applies`() {
+        this.spring.register(HttpRedirectMatcherAndFunctionConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/secure")
+                .exchange()
+                .expectStatus().isNotFound
+
+        val result = this.client.get()
+                .uri("/")
+                .header("X-Requires-Https", "required")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasScheme("https")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HttpRedirectMatcherAndFunctionConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                redirectToHttps {
+                    httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure"))
+                    httpsRedirectWhen {
+                        it.request.headers.containsKey("X-Requires-Https")
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when port mapper configured then redirected to HTTPS port`() {
+        this.spring.register(PortMapperConfig::class.java).autowire()
+
+        val result = this.client.get()
+                .uri("http://localhost:543")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location).hasScheme("https").hasPort(123)
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PortMapperConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            val customPortMapper = PortMapperImpl()
+            customPortMapper.setPortMappings(Collections.singletonMap("543", "123"))
+            return http {
+                redirectToHttps {
+                    portMapper = customPortMapper
+                }
+            }
+        }
+    }
+}

+ 244 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt

@@ -0,0 +1,244 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+
+/**
+ * Tests for [ServerLogoutDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerLogoutDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `logout when defaults used then redirects to login page`() {
+        this.spring.register(LogoutConfig::class.java).autowire()
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/logout")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location)
+                    .hasPath("/login")
+                    .hasParameter("logout")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class LogoutConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                logout { }
+            }
+        }
+    }
+
+    @Test
+    fun `logout when custom logout URL then custom URL redirects to login page`() {
+        this.spring.register(CustomUrlConfig::class.java).autowire()
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/custom-logout")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location)
+                    .hasPath("/login")
+                    .hasParameter("logout")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomUrlConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                logout {
+                    logoutUrl = "/custom-logout"
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `logout when custom requires logout matcher then matching request redirects to login page`() {
+        this.spring.register(RequiresLogoutConfig::class.java).autowire()
+
+        val result = this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/custom-logout")
+                .exchange()
+                .expectStatus().is3xxRedirection
+                .returnResult(String::class.java)
+
+        result.assertWithDiagnostics {
+            assertThat(result.responseHeaders.location)
+                    .hasPath("/login")
+                    .hasParameter("logout")
+        }
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class RequiresLogoutConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                logout {
+                    requiresLogout = PathPatternParserServerWebExchangeMatcher("/custom-logout")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `logout when custom logout handler then custom handler invoked`() {
+        this.spring.register(CustomLogoutHandlerConfig::class.java).autowire()
+
+        `when`(CustomLogoutHandlerConfig.LOGOUT_HANDLER.logout(any(), any()))
+                .thenReturn(Mono.empty())
+
+        this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/logout")
+                .exchange()
+
+        verify<ServerLogoutHandler>(CustomLogoutHandlerConfig.LOGOUT_HANDLER)
+                .logout(any(), any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomLogoutHandlerConfig {
+        companion object {
+            var LOGOUT_HANDLER: ServerLogoutHandler = mock(ServerLogoutHandler::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                logout {
+                    logoutHandler = LOGOUT_HANDLER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `logout when custom logout success handler then custom handler invoked`() {
+        this.spring.register(CustomLogoutSuccessHandlerConfig::class.java).autowire()
+
+        this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/logout")
+                .exchange()
+
+        verify<ServerLogoutSuccessHandler>(CustomLogoutSuccessHandlerConfig.LOGOUT_HANDLER)
+                .onLogoutSuccess(any(), any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomLogoutSuccessHandlerConfig {
+        companion object {
+            var LOGOUT_HANDLER: ServerLogoutSuccessHandler = mock(ServerLogoutSuccessHandler::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                logout {
+                    logoutSuccessHandler = LOGOUT_HANDLER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `logout when disabled then logout URL not found`() {
+        this.spring.register(LogoutDisabledConfig::class.java).autowire()
+
+        this.client
+                .mutateWith(csrf())
+                .post()
+                .uri("/logout")
+                .exchange()
+                .expectStatus().isNotFound
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class LogoutDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, permitAll)
+                }
+                logout {
+                    disable()
+                }
+            }
+        }
+    }
+}

+ 223 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt

@@ -0,0 +1,223 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+
+/**
+ * Tests for [ServerOAuth2ClientDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerOAuth2ClientDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `OAuth2 client when custom client registration repository then bean is not required`() {
+        this.spring.register(ClientRepoConfig::class.java).autowire()
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ClientRepoConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2Client {
+                    clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository(
+                            CommonOAuth2Provider.GOOGLE
+                                    .getBuilder("google").clientId("clientId").clientSecret("clientSecret")
+                                    .build()
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 client when authorization request repository configured then custom repository used`() {
+        this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
+
+        this.client.get()
+                .uri {
+                    it.path("/")
+                            .queryParam(OAuth2ParameterNames.CODE, "code")
+                            .queryParam(OAuth2ParameterNames.STATE, "state")
+                            .build()
+                }
+                .exchange()
+
+        verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).loadAuthorizationRequest(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthorizationRequestRepositoryConfig {
+        companion object {
+            var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
+                    as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Client {
+                    authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 client when authentication converter configured then custom converter used`() {
+        this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire()
+
+        `when`(AuthenticationConverterConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any()))
+                .thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode()
+                        .authorizationUri("https://example.com/login/oauth/authorize")
+                        .clientId("clientId")
+                        .redirectUri("/authorize/oauth2/code/google")
+                        .build()))
+
+        this.client.get()
+                .uri {
+                    it.path("/authorize/oauth2/code/google")
+                            .queryParam(OAuth2ParameterNames.CODE, "code")
+                            .queryParam(OAuth2ParameterNames.STATE, "state")
+                            .build()
+                }
+                .exchange()
+
+        verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationConverterConfig {
+        companion object {
+            var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
+                    as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
+            var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Client {
+                    authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
+                    authenticationConverter = AUTHENTICATION_CONVERTER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 client when authentication manager configured then custom manager used`() {
+        this.spring.register(AuthenticationManagerConfig::class.java, ClientConfig::class.java).autowire()
+
+        `when`(AuthenticationManagerConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any()))
+                .thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode()
+                        .authorizationUri("https://example.com/login/oauth/authorize")
+                        .clientId("clientId")
+                        .redirectUri("/authorize/oauth2/code/google")
+                        .build()))
+        `when`(AuthenticationManagerConfig.AUTHENTICATION_CONVERTER.convert(any()))
+                .thenReturn(Mono.just(TestingAuthenticationToken("a", "b", "c")))
+
+        this.client.get()
+                .uri {
+                    it.path("/authorize/oauth2/code/google")
+                            .queryParam(OAuth2ParameterNames.CODE, "code")
+                            .queryParam(OAuth2ParameterNames.STATE, "state")
+                            .build()
+                }
+                .exchange()
+
+        verify(AuthenticationManagerConfig.AUTHENTICATION_MANAGER).authenticate(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationManagerConfig {
+        companion object {
+            var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
+                    as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
+            var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
+            var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Client {
+                    authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
+                    authenticationConverter = AUTHENTICATION_CONVERTER
+                    authenticationManager = AUTHENTICATION_MANAGER
+                }
+            }
+        }
+    }
+
+    @Configuration
+    open class ClientConfig {
+        @Bean
+        open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository {
+            return InMemoryReactiveClientRegistrationRepository(
+                    CommonOAuth2Provider.GOOGLE
+                            .getBuilder("google").clientId("clientId").clientSecret("clientSecret")
+                            .build()
+            )
+        }
+    }
+}

+ 201 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt

@@ -0,0 +1,201 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
+import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerOAuth2LoginDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerOAuth2LoginDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `oauth2Login when custom client registration repository then bean is not required`() {
+        this.spring.register(ClientRepoConfig::class.java).autowire()
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ClientRepoConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2Login {
+                    clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository(
+                            CommonOAuth2Provider.GOOGLE
+                                    .getBuilder("google").clientId("clientId").clientSecret("clientSecret")
+                                    .build()
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `login page when OAuth2 login configured then default login page created`() {
+        this.spring.register(OAuth2LoginConfig::class.java, ClientConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/login")
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class OAuth2LoginConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Login { }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 login when authorization request repository configured then custom repository used`() {
+        this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/login/oauth2/code/google")
+                .exchange()
+
+        verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).removeAuthorizationRequest(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthorizationRequestRepositoryConfig {
+        companion object {
+            var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
+                    as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Login {
+                    authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 login when authentication matcher configured then custom matcher used`() {
+        this.spring.register(AuthenticationMatcherConfig::class.java, ClientConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+
+        verify(AuthenticationMatcherConfig.AUTHENTICATION_MATCHER).matches(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationMatcherConfig {
+        companion object {
+            var AUTHENTICATION_MATCHER: ServerWebExchangeMatcher = mock(ServerWebExchangeMatcher::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Login {
+                    authenticationMatcher = AUTHENTICATION_MATCHER
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `OAuth2 login when authentication converter configured then custom converter used`() {
+        this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/login/oauth2/code/google")
+                .exchange()
+
+        verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationConverterConfig {
+        companion object {
+            var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                oauth2Login {
+                    authenticationConverter = AUTHENTICATION_CONVERTER
+                }
+            }
+        }
+    }
+
+    @Configuration
+    open class ClientConfig {
+        @Bean
+        open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository {
+            return InMemoryReactiveClientRegistrationRepository(
+                    CommonOAuth2Provider.GOOGLE
+                            .getBuilder("google").clientId("clientId").clientSecret("clientSecret")
+                            .build()
+            )
+        }
+    }
+}

+ 204 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt

@@ -0,0 +1,204 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpStatus
+import org.springframework.http.server.reactive.ServerHttpRequest
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
+import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import org.springframework.web.server.ServerWebExchange
+import java.math.BigInteger
+import java.security.KeyFactory
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.RSAPublicKeySpec
+
+/**
+ * Tests for [ServerOAuth2ResourceServerDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerOAuth2ResourceServerDslTests {
+    private val validJwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q"
+
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when custom access denied handler configured then custom handler used`() {
+        this.spring.register(AccessDeniedHandlerConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { it.setBearerAuth(validJwt) }
+                .exchange()
+                .expectStatus().isSeeOther
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AccessDeniedHandlerConfig {
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, hasAuthority("ADMIN"))
+                }
+                oauth2ResourceServer {
+                    accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER)
+                    jwt {
+                        publicKey = publicKey()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom entry point configured then custom entry point used`() {
+        this.spring.register(AuthenticationEntryPointConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectStatus().isSeeOther
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationEntryPointConfig {
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    authenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.SEE_OTHER)
+                    jwt {
+                        publicKey = publicKey()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom bearer token converter configured then custom converter used`() {
+        this.spring.register(BearerTokenConverterConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { it.setBearerAuth(validJwt) }
+                .exchange()
+
+        verify(BearerTokenConverterConfig.CONVERTER).convert(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class BearerTokenConverterConfig {
+        companion object {
+            val CONVERTER: ServerBearerTokenAuthenticationConverter = mock(ServerBearerTokenAuthenticationConverter::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    bearerTokenConverter = CONVERTER
+                    jwt {
+                        publicKey = publicKey()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom authentication manager resolver configured then custom resolver used`() {
+        this.spring.register(AuthenticationManagerResolverConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { it.setBearerAuth(validJwt) }
+                .exchange()
+
+        verify(AuthenticationManagerResolverConfig.RESOLVER).resolve(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationManagerResolverConfig {
+        companion object {
+            val RESOLVER: ReactiveAuthenticationManagerResolver<ServerWebExchange> =
+                    mock(ReactiveAuthenticationManagerResolver::class.java) as ReactiveAuthenticationManagerResolver<ServerWebExchange>
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    authenticationManagerResolver = RESOLVER
+                }
+            }
+        }
+    }
+
+    companion object {
+        private fun publicKey(): RSAPublicKey {
+            val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"
+            val exponent = "65537"
+            val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent))
+            val factory = KeyFactory.getInstance("RSA")
+            return factory.generatePublic(spec) as RSAPublicKey
+        }
+    }
+}

+ 104 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.savedrequest.ServerRequestCache
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+
+/**
+ * Tests for [ServerRequestCacheDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerRequestCacheDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `GET when request cache enabled then redirected to cached page`() {
+        this.spring.register(RequestCacheConfig::class.java, UserDetailsConfig::class.java).autowire()
+        `when`(RequestCacheConfig.REQUEST_CACHE.removeMatchingRequest(any())).thenReturn(Mono.empty())
+
+        this.client.get()
+                .uri("/test")
+                .exchange()
+
+        verify(RequestCacheConfig.REQUEST_CACHE).saveRequest(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class RequestCacheConfig {
+        companion object {
+            var REQUEST_CACHE: ServerRequestCache = Mockito.mock(ServerRequestCache::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                formLogin { }
+                requestCache {
+                    requestCache = REQUEST_CACHE
+                }
+            }
+        }
+    }
+
+    @Configuration
+    open class UserDetailsConfig {
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("user")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+}

+ 237 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt

@@ -0,0 +1,237 @@
+/*
+ * Copyright 2002-2020 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.web.server
+
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.io.ClassPathResource
+import org.springframework.http.client.reactive.ClientHttpConnector
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator
+import org.springframework.http.server.reactive.SslInfo
+import org.springframework.lang.Nullable
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.core.annotation.AuthenticationPrincipal
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
+import org.springframework.test.web.reactive.server.MockServerConfigurer
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.reactive.server.WebTestClientConfigurer
+import org.springframework.test.web.reactive.server.expectBody
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+import org.springframework.web.server.ServerWebExchange
+import org.springframework.web.server.ServerWebExchangeDecorator
+import org.springframework.web.server.WebFilter
+import org.springframework.web.server.WebFilterChain
+import org.springframework.web.server.adapter.WebHttpHandlerBuilder
+import reactor.core.publisher.Mono
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+
+/**
+ * Tests for [ServerX509Dsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerX509DslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `x509 when configured with defaults then user authenticated with expected username`() {
+        this.spring
+                .register(X509DefaultConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java)
+                .autowire()
+        val certificate = loadCert<X509Certificate>("rod.cer")
+
+        this.client
+                .mutateWith(mockX509(certificate))
+                .get()
+                .uri("/username")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("rod")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class X509DefaultConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                x509 { }
+            }
+        }
+    }
+
+    @Test
+    fun `x509 when principal extractor customized then custom principal extractor used`() {
+        this.spring
+                .register(PrincipalExtractorConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java)
+                .autowire()
+        val certificate = loadCert<X509Certificate>("rodatexampledotcom.cer")
+
+        this.client
+                .mutateWith(mockX509(certificate))
+                .get()
+                .uri("/username")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("rod")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PrincipalExtractorConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            val customPrincipalExtractor = SubjectDnX509PrincipalExtractor()
+            customPrincipalExtractor.setSubjectDnRegex("CN=(.*?)@example.com(?:,|$)")
+            return http {
+                x509 {
+                    principalExtractor = customPrincipalExtractor
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `x509 when authentication manager customized then custom authentication manager used`() {
+        this.spring
+                .register(AuthenticationManagerConfig::class.java, UsernameController::class.java)
+                .autowire()
+        val certificate = loadCert<X509Certificate>("rod.cer")
+
+        this.client
+                .mutateWith(mockX509(certificate))
+                .get()
+                .uri("/username")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody<String>().isEqualTo("rod")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class AuthenticationManagerConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                x509 {
+                    authenticationManager = ReactivePreAuthenticatedAuthenticationManager(userDetailsService())
+                }
+            }
+        }
+
+        fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("rod")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+
+    @RestController
+    class UsernameController {
+        @GetMapping("/username")
+        fun principal(@AuthenticationPrincipal user: User?): String {
+            return user!!.username
+        }
+    }
+
+    @Configuration
+    open class UserDetailsConfig {
+        @Bean
+        open fun userDetailsService(): MapReactiveUserDetailsService {
+            val user = User.withDefaultPasswordEncoder()
+                    .username("rod")
+                    .password("password")
+                    .roles("USER")
+                    .build()
+            return MapReactiveUserDetailsService(user)
+        }
+    }
+
+    private fun mockX509(certificate: X509Certificate): X509Mutator {
+        return X509Mutator(certificate)
+    }
+
+    private class X509Mutator internal constructor(private var certificate: X509Certificate) : WebTestClientConfigurer, MockServerConfigurer {
+
+        override fun afterConfigurerAdded(builder: WebTestClient.Builder,
+                                          @Nullable httpHandlerBuilder: WebHttpHandlerBuilder?,
+                                          @Nullable connector: ClientHttpConnector?) {
+            val filter = SetSslInfoWebFilter(certificate)
+            httpHandlerBuilder!!.filters { filters: MutableList<WebFilter?> -> filters.add(0, filter) }
+        }
+    }
+
+    private class SetSslInfoWebFilter(var certificate: X509Certificate) : WebFilter {
+
+        override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
+            return chain.filter(decorate(exchange))
+        }
+
+        private fun decorate(exchange: ServerWebExchange): ServerWebExchange {
+            val decorated: ServerHttpRequestDecorator = object : ServerHttpRequestDecorator(exchange.request) {
+                override fun getSslInfo(): SslInfo {
+                    val sslInfo = mock(SslInfo::class.java)
+                    `when`(sslInfo.sessionId).thenReturn("sessionId")
+                    `when`(sslInfo.peerCertificates).thenReturn(arrayOf(certificate))
+                    return sslInfo
+                }
+            }
+            return object : ServerWebExchangeDecorator(exchange) {
+                override fun getRequest(): org.springframework.http.server.reactive.ServerHttpRequest {
+                    return decorated
+                }
+            }
+        }
+    }
+
+    private fun <T : Certificate> loadCert(location: String): T {
+        ClassPathResource(location).inputStream.use { inputStream ->
+            val certFactory = CertificateFactory.getInstance("X.509")
+            return certFactory.generateCertificate(inputStream) as T
+        }
+    }
+}

+ 104 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDslTests.kt

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerCacheControlDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerCacheControlDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when cache control configured then cache headers in response`() {
+        this.spring.register(CacheControlConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
+                .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
+                .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CacheControlConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    cache { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when cache control disabled then no cache headers in response`() {
+        this.spring.register(CacheControlDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL)
+                .expectHeader().doesNotExist(HttpHeaders.EXPIRES)
+                .expectHeader().doesNotExist(HttpHeaders.PRAGMA)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CacheControlDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    cache {
+                        disable()
+                    }
+                }
+            }
+        }
+    }
+}

+ 125 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDslTests.kt

@@ -0,0 +1,125 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerContentSecurityPolicyDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerContentSecurityPolicyDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when content security policy configured then content security policy header in response`() {
+        this.spring.register(ContentSecurityPolicyConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ContentSecurityPolicyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    contentSecurityPolicy { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom policy directives then custom policy directive in response header`() {
+        this.spring.register(CustomPolicyDirectivesConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'; script-src trustedscripts.example.com")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomPolicyDirectivesConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    contentSecurityPolicy {
+                        policyDirectives = "default-src 'self'; script-src trustedscripts.example.com"
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when report only configured then content security policy report only header in response`() {
+        this.spring.register(ReportOnlyConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY, "default-src 'self'")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ReportOnlyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    contentSecurityPolicy {
+                        reportOnly = true
+                    }
+                }
+            }
+        }
+    }
+}

+ 100 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDslTests.kt

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerContentTypeOptionsDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerContentTypeOptionsDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when content type options configured then header in response`() {
+        this.spring.register(ContentTypeOptionsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ContentTypeOptionsConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    contentTypeOptions { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when content type options disabled then no content type options header in response`() {
+        this.spring.register(ContentTypeOptionsDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ContentTypeOptionsDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    contentTypeOptions {
+                        disable()
+                    }
+                }
+            }
+        }
+    }
+}

+ 126 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDslTests.kt

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerFrameOptionsDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerFrameOptionsDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when frame options configured then header in response`() {
+        this.spring.register(FrameOptionsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class FrameOptionsConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    frameOptions { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when frame options disabled then no frame options header in response`() {
+        this.spring.register(FrameOptionsDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class FrameOptionsDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    frameOptions {
+                        disable()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when frame options mode set then frame options response header has mode value`() {
+        this.spring.register(CustomModeConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN.name)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomModeConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    frameOptions {
+                        mode = XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN
+                    }
+                }
+            }
+        }
+    }
+}

+ 176 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDslTests.kt

@@ -0,0 +1,176 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import java.time.Duration
+
+/**
+ * Tests for [ServerReferrerPolicyDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerHttpStrictTransportSecurityDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when hsts configured then hsts header in response`() {
+        this.spring.register(HstsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HstsConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    hsts { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when hsts disabled then no hsts header in response`() {
+        this.spring.register(HstsDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class HstsDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    hsts {
+                        disable()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when max age set then max age in response header`() {
+        this.spring.register(MaxAgeConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=1 ; includeSubDomains")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class MaxAgeConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    hsts {
+                        maxAge = Duration.ofSeconds(1)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when includeSubdomains false then includeSubdomains not in response header`() {
+        this.spring.register(IncludeSubdomainsConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class IncludeSubdomainsConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    hsts {
+                        includeSubdomains = false
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when preload true then preload included in response header`() {
+        this.spring.register(PreloadConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("https://example.com")
+                .exchange()
+                .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains ; preload")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PreloadConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    hsts {
+                        preload = true
+                    }
+                }
+            }
+        }
+    }
+}

+ 101 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDslTests.kt

@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerReferrerPolicyDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerReferrerPolicyDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when referrer policy configured then referrer policy header in response`() {
+        this.spring.register(ReferrerPolicyConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER.policy)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class ReferrerPolicyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    referrerPolicy { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when custom policy configured then custom policy in response header`() {
+        this.spring.register(CustomPolicyConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN.policy)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomPolicyConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    referrerPolicy {
+                        policy = ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN
+                    }
+                }
+            }
+        }
+    }
+}

+ 100 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDslTests.kt

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2002-2020 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.web.server.headers
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerXssProtectionDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerXssProtectionDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when xss protection configured then xss header in response`() {
+        this.spring.register(XssConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class XssConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    xssProtection { }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `request when xss protection disabled then no xss header in response`() {
+        this.spring.register(XssDisabledConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .exchange()
+                .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class XssDisabledConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                headers {
+                    xssProtection {
+                        disable()
+                    }
+                }
+            }
+        }
+    }
+}

+ 272 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDslTests.kt

@@ -0,0 +1,272 @@
+/*
+ * Copyright 2002-2020 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.web.server.oauth2.resourceserver
+
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.core.convert.converter.Converter
+import org.springframework.http.HttpHeaders
+import org.springframework.security.authentication.AbstractAuthenticationToken
+import org.springframework.security.authentication.TestingAuthenticationToken
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames
+import org.springframework.security.oauth2.jwt.Jwt
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.reactive.config.EnableWebFlux
+import reactor.core.publisher.Mono
+import java.math.BigInteger
+import java.security.KeyFactory
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.RSAPublicKeySpec
+import javax.annotation.PreDestroy
+
+/**
+ * Tests for [ServerJwtDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerJwtDslTests {
+
+    private val expired = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzUwMzc4OTd9.jqZDDjfc2eysX44lHXEIr9XFd2S8vjIZHCccZU-dRWMRJNsQ1QN5VNnJGklqJBXJR4qgla6cmVqPOLkUHDb0sL0nxM5XuzQaG5ZzKP81RV88shFyAiT0fD-6nl1k-Fai-Fu-VkzSpNXgeONoTxDaYhdB-yxmgrgsApgmbOTE_9AcMk-FQDXQ-pL9kynccFGV0lZx4CA7cyknKN7KBxUilfIycvXODwgKCjj_1WddLTCNGYogJJSg__7NoxzqbyWd3udbHVjqYq7GsMMrGB4_2kBD4CkghOSNcRHbT_DIXowxfAVT7PAg7Q0E5ruZsr2zPZacEUDhJ6-wbvlA0FAOUg"
+    private val messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q"
+    private val jwkSet = "{\n" +
+            "  \"keys\":[\n" +
+            "    {\n" +
+            "      \"kty\":\"RSA\",\n" +
+            "      \"e\":\"AQAB\",\n" +
+            "      \"use\":\"sig\",\n" +
+            "      \"kid\":\"one\",\n" +
+            "      \"n\":\"0IUjrPZDz-3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh-64VhYUa-vz17zYCmuB8fFj4XHE3MLkWIG-AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn-85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp-Cln6JROJcZUIRJ-QvS6QONWeS2407uQmS-i-lybsqaH0ldYC7NBEBA5inPQ\"\n" +
+            "    }\n" +
+            "  ]\n" +
+            "}\n"
+
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `request when JWT configured with public key and valid token then responds with ok`() {
+        this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) }
+                .exchange()
+                .expectStatus().isOk
+    }
+
+    @Test
+    fun `request when JWT configured with public key and expired token then responds with unauthorized`() {
+        this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { headers: HttpHeaders -> headers.setBearerAuth(expired) }
+                .exchange()
+                .expectStatus().isUnauthorized
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PublicKeyConfig {
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    jwt {
+                        publicKey = publicKey()
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `jwt when using custom JWT decoded then custom decoded used`() {
+        this.spring.register(CustomDecoderConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/")
+                .headers { headers: HttpHeaders -> headers.setBearerAuth("token") }
+                .exchange()
+
+        verify(CustomDecoderConfig.JWT_DECODER).decode("token")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomDecoderConfig {
+        companion object {
+            var JWT_DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    jwt {
+                        jwtDecoder = JWT_DECODER
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `jwt when using custom JWK Set URI then custom URI used`() {
+        this.spring.register(CustomJwkSetUriConfig::class.java).autowire()
+
+        CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
+
+        this.client.get()
+                .uri("/")
+                .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) }
+                .exchange()
+
+        val recordedRequest = CustomJwkSetUriConfig.MOCK_WEB_SERVER.takeRequest()
+        assertThat(recordedRequest.path).isEqualTo("/.well-known/jwks.json")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomJwkSetUriConfig {
+        companion object {
+            var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    jwt {
+                        jwkSetUri = mockWebServer().url("/.well-known/jwks.json").toString()
+                    }
+                }
+            }
+        }
+
+        @Bean
+        open fun mockWebServer(): MockWebServer {
+            return MOCK_WEB_SERVER
+        }
+
+        @PreDestroy
+        open fun shutdown() {
+            MOCK_WEB_SERVER.shutdown()
+        }
+    }
+
+
+    @Test
+    fun `opaque token when custom JWT authentication converter then converter used`() {
+        this.spring.register(CustomJwtAuthenticationConverterConfig::class.java).autowire()
+        `when`(CustomJwtAuthenticationConverterConfig.DECODER.decode(anyString())).thenReturn(
+                Mono.just(Jwt.withTokenValue("token")
+                        .header("alg", "none")
+                        .claim(IdTokenClaimNames.SUB, "user")
+                        .build()))
+        `when`(CustomJwtAuthenticationConverterConfig.CONVERTER.convert(any()))
+                .thenReturn(Mono.just(TestingAuthenticationToken("test", "this", "ROLE")))
+
+        this.client.get()
+                .uri("/")
+                .headers { headers: HttpHeaders -> headers.setBearerAuth("token") }
+                .exchange()
+
+        verify(CustomJwtAuthenticationConverterConfig.CONVERTER).convert(any())
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomJwtAuthenticationConverterConfig {
+        companion object {
+            var CONVERTER: Converter<Jwt, out Mono<AbstractAuthenticationToken>> = mock(Converter::class.java) as Converter<Jwt, out Mono<AbstractAuthenticationToken>>
+            var DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java)
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    jwt {
+                        jwtAuthenticationConverter = CONVERTER
+                    }
+                }
+            }
+        }
+
+        @Bean
+        open fun jwtDecoder(): ReactiveJwtDecoder {
+            return DECODER
+        }
+    }
+
+    @RestController
+    internal class BaseController {
+        @GetMapping
+        fun index() {
+        }
+    }
+
+    companion object {
+        private fun publicKey(): RSAPublicKey {
+            val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"
+            val exponent = "65537"
+            val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent))
+            val factory = KeyFactory.getInstance("RSA")
+            return factory.generatePublic(spec) as RSAPublicKey
+        }
+    }
+}

+ 203 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDslTests.kt

@@ -0,0 +1,203 @@
+/*
+ * Copyright 2002-2020 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.web.server.oauth2.resourceserver
+
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+import javax.annotation.PreDestroy
+
+/**
+ * Tests for [ServerOpaqueTokenDsl]
+ *
+ * @author Eleftheria Stein
+ */
+class ServerOpaqueTokenDslTests {
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `opaque token when using defaults then uses introspector bean`() {
+        this.spring.register(IntrospectorBeanConfig::class.java).autowire()
+
+        IntrospectorBeanConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
+
+        this.client.get()
+                .uri("/")
+                .header(HttpHeaders.AUTHORIZATION, "Bearer token")
+                .exchange()
+
+        val recordedRequest = IntrospectorBeanConfig.MOCK_WEB_SERVER.takeRequest()
+        assertThat(recordedRequest.path).isEqualTo("/introspect")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class IntrospectorBeanConfig {
+        companion object {
+            var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    opaqueToken { }
+                }
+            }
+        }
+
+        @Bean
+        open fun mockWebServer(): MockWebServer {
+            return MOCK_WEB_SERVER
+        }
+
+        @PreDestroy
+        open fun shutdown() {
+            MOCK_WEB_SERVER.shutdown()
+        }
+
+        @Bean
+        open fun tokenIntrospectionClient(): ReactiveOpaqueTokenIntrospector {
+            return NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspect").toString(), "client", "secret")
+        }
+    }
+
+    @Test
+    fun `opaque token when using custom introspector then introspector used`() {
+        this.spring.register(CustomIntrospectorConfig::class.java).autowire()
+
+        CustomIntrospectorConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
+
+        this.client.get()
+                .uri("/")
+                .header(HttpHeaders.AUTHORIZATION, "Bearer token")
+                .exchange()
+
+        val recordedRequest = CustomIntrospectorConfig.MOCK_WEB_SERVER.takeRequest()
+        assertThat(recordedRequest.path).isEqualTo("/introspector")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomIntrospectorConfig {
+        companion object {
+            var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    opaqueToken {
+                        introspector = NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspector").toString(), "client", "secret")
+                    }
+                }
+            }
+        }
+
+        @Bean
+        open fun mockWebServer(): MockWebServer {
+            return MOCK_WEB_SERVER
+        }
+
+        @PreDestroy
+        open fun shutdown() {
+            MOCK_WEB_SERVER.shutdown()
+        }
+    }
+
+    @Test
+    fun `opaque token when using custom introspection URI and credentials then custom used`() {
+        this.spring.register(CustomIntrospectionUriAndCredentialsConfig::class.java).autowire()
+
+        CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
+
+        this.client.get()
+                .uri("/")
+                .header(HttpHeaders.AUTHORIZATION, "Bearer token")
+                .exchange()
+
+        val recordedRequest = CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.takeRequest()
+        assertThat(recordedRequest.path).isEqualTo("/introspection-uri")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class CustomIntrospectionUriAndCredentialsConfig {
+        companion object {
+            var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
+        }
+
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oauth2ResourceServer {
+                    opaqueToken {
+                        introspectionUri = mockWebServer().url("/introspection-uri").toString()
+                        introspectionClientCredentials("client", "secret")
+                    }
+                }
+            }
+        }
+
+        @Bean
+        open fun mockWebServer(): MockWebServer {
+            return MOCK_WEB_SERVER
+        }
+
+        @PreDestroy
+        open fun shutdown() {
+            MOCK_WEB_SERVER.shutdown()
+        }
+    }
+}