Idiomatic Kotlin DSL for server HTTP security

Issue: gh-5558
This commit is contained in:
Eleftheria Stein 2020-04-07 10:52:37 -04:00
parent 6017510fdd
commit 39e09e4ca5
50 changed files with 6532 additions and 0 deletions

View File

@ -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>)
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}

View File

@ -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) }
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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!!)
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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) }
}
}
}

View File

@ -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)
}
}
}

View File

@ -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() }
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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'")
}
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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()
)
}
}
}

View File

@ -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()
)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}