diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt new file mode 100644 index 0000000000..849413d2a8 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager +import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager +import org.springframework.security.authorization.AuthorizationDecision +import org.springframework.security.authorization.ReactiveAuthorizationManager +import org.springframework.security.core.Authentication +import org.springframework.security.web.server.authorization.AuthorizationContext +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers +import org.springframework.security.web.util.matcher.RequestMatcher +import reactor.core.publisher.Mono + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] exchange authorization using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class AuthorizeExchangeDsl { + private val authorizationRules = mutableListOf() + + /** + * 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 = 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 = authenticated) { + authorizationRules.add(PatternExchangeAuthorizationRule(antPattern, access)) + } + + /** + * Matches any exchange. + */ + val anyExchange: ServerWebExchangeMatcher = ServerWebExchangeMatchers.anyExchange() + + /** + * Allow access for anyone. + */ + val permitAll: ReactiveAuthorizationManager = + ReactiveAuthorizationManager { _: Mono, _: AuthorizationContext -> Mono.just(AuthorizationDecision(true)) } + + /** + * Deny access for everyone. + */ + val denyAll: ReactiveAuthorizationManager = + ReactiveAuthorizationManager { _: Mono, _: AuthorizationContext -> Mono.just(AuthorizationDecision(false)) } + + /** + * Require a specific role. This is a shortcut for [hasAuthority]. + */ + fun hasRole(role: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasRole(role) + + /** + * Require any specific role. This is a shortcut for [hasAnyAuthority]. + */ + fun hasAnyRole(vararg roles: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAnyRole(*roles) + + /** + * Require a specific authority. + */ + fun hasAuthority(authority: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAuthority(authority) + + /** + * Require any authority. + */ + fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAnyAuthority(*authorities) + + /** + * Require an authenticated user. + */ + val authenticated: ReactiveAuthorizationManager = + AuthenticatedReactiveAuthorizationManager.authenticated() + + 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) : ExchangeAuthorizationRule(rule) + + private data class PatternExchangeAuthorizationRule(val pattern: String, + override val rule: ReactiveAuthorizationManager) : ExchangeAuthorizationRule(rule) + + private abstract class ExchangeAuthorizationRule(open val rule: ReactiveAuthorizationManager) +} + diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt new file mode 100644 index 0000000000..912e9ea979 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] anonymous authentication using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property key the key to identify tokens created for anonymous authentication + * @property principal the principal for [Authentication] objects of anonymous users + * @property authorities the [Authentication.getAuthorities] for anonymous users + * @property authenticationFilter the [AnonymousAuthenticationWebFilter] used to populate + * an anonymous user. + */ +class ServerAnonymousDsl { + var key: String? = null + var principal: Any? = null + var authorities: List? = 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() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt new file mode 100644 index 0000000000..c227a6b336 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.web.cors.reactive.CorsConfigurationSource + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] CORS headers using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property configurationSource the [CorsConfigurationSource] to use. + */ +class ServerCorsDsl { + var configurationSource: CorsConfigurationSource? = null + + private var disabled = false + + /** + * Disables CORS support within Spring Security. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.CorsSpec) -> Unit { + return { cors -> + configurationSource?.also { cors.configurationSource(configurationSource) } + if (disabled) { + cors.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt new file mode 100644 index 0000000000..14b6b10cc8 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] CSRF protection using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property accessDeniedHandler the [ServerAccessDeniedHandler] used when a CSRF token is invalid. + * @property csrfTokenRepository the [ServerCsrfTokenRepository] used to persist the CSRF token. + * @property requireCsrfProtectionMatcher the [ServerWebExchangeMatcher] used to determine when CSRF protection + * is enabled. + */ +class ServerCsrfDsl { + var accessDeniedHandler: ServerAccessDeniedHandler? = null + var csrfTokenRepository: ServerCsrfTokenRepository? = null + var requireCsrfProtectionMatcher: ServerWebExchangeMatcher? = null + + private var disabled = false + + /** + * Disables CSRF protection + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.CsrfSpec) -> Unit { + return { csrf -> + accessDeniedHandler?.also { csrf.accessDeniedHandler(accessDeniedHandler) } + csrfTokenRepository?.also { csrf.csrfTokenRepository(csrfTokenRepository) } + requireCsrfProtectionMatcher?.also { csrf.requireCsrfProtectionMatcher(requireCsrfProtectionMatcher) } + if (disabled) { + csrf.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt new file mode 100644 index 0000000000..bbaa8ff9a3 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] exception handling using idiomatic Kotlin + * code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use when + * the application request authentication + * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use when an + * authenticated user does not hold a required authority + */ +class ServerExceptionHandlingDsl { + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var accessDeniedHandler: ServerAccessDeniedHandler? = null + + internal fun get(): (ServerHttpSecurity.ExceptionHandlingSpec) -> Unit { + return { exceptionHandling -> + authenticationEntryPoint?.also { exceptionHandling.authenticationEntryPoint(authenticationEntryPoint) } + accessDeniedHandler?.also { exceptionHandling.accessDeniedHandler(accessDeniedHandler) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt new file mode 100644 index 0000000000..71e9a9a945 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ReactorContextWebFilter +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate. + * @property loginPage the url to redirect to which provides a form to log in (i.e. "/login"). + * If this is customized: + * - The default log in & log out page are no longer provided + * - The application must render a log in page at the provided URL + * - The application must render an authentication error page at the provided URL + "?error" + * - Authentication will occur for POST to the provided URL + * @property authenticationEntryPoint configures how to request for authentication. + * @property requiresAuthenticationMatcher configures when authentication is performed. + * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after + * authentication success. + * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used to handle + * a failed authentication. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save + * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the + * [ReactorContextWebFilter] must be configured to be able to load the value (they are not + * implicitly linked). + */ +class ServerFormLoginDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var loginPage: String? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var requiresAuthenticationMatcher: ServerWebExchangeMatcher? = null + var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null + var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null + var securityContextRepository: ServerSecurityContextRepository? = null + + private var disabled = false + + /** + * Disables HTTP basic authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.FormLoginSpec) -> Unit { + return { formLogin -> + authenticationManager?.also { formLogin.authenticationManager(authenticationManager) } + loginPage?.also { formLogin.loginPage(loginPage) } + authenticationEntryPoint?.also { formLogin.authenticationEntryPoint(authenticationEntryPoint) } + requiresAuthenticationMatcher?.also { formLogin.requiresAuthenticationMatcher(requiresAuthenticationMatcher) } + authenticationSuccessHandler?.also { formLogin.authenticationSuccessHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { formLogin.authenticationFailureHandler(authenticationFailureHandler) } + securityContextRepository?.also { formLogin.securityContextRepository(securityContextRepository) } + if (disabled) { + formLogin.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt new file mode 100644 index 0000000000..ecf73bd566 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.web.server.headers.* +import org.springframework.security.web.server.header.* + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class ServerHeadersDsl { + private var contentTypeOptions: ((ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit)? = null + private var xssProtection: ((ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit)? = null + private var cacheControl: ((ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit)? = null + private var hsts: ((ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit)? = null + private var frameOptions: ((ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit)? = null + private var contentSecurityPolicy: ((ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit)? = null + private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null + private var featurePolicyDirectives: String? = null + + private var disabled = false + + /** + * Configures the [ContentTypeOptionsServerHttpHeadersWriter] which inserts the X-Content-Type-Options header + * + * @param contentTypeOptionsConfig the customization to apply to the header + */ + fun contentTypeOptions(contentTypeOptionsConfig: ServerContentTypeOptionsDsl.() -> Unit) { + this.contentTypeOptions = ServerContentTypeOptionsDsl().apply(contentTypeOptionsConfig).get() + } + + /** + * Note this is not comprehensive XSS protection! + * + *

+ * Allows customizing the [XXssProtectionServerHttpHeadersWriter] which adds the X-XSS-Protection header + *

+ * + * @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: + *
    + *
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • + *
  • Pragma: no-cache
  • + *
  • Expires: 0
  • + *
+ * + * @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 HTTP Strict Transport Security + * (HSTS). + * + * @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 Content Security Policy (CSP) Level 2. + * + * @param contentSecurityPolicyConfig the customization to apply to the header + */ + fun contentSecurityPolicy(contentSecurityPolicyConfig: ServerContentSecurityPolicyDsl.() -> Unit) { + this.contentSecurityPolicy = ServerContentSecurityPolicyDsl().apply(contentSecurityPolicyConfig).get() + } + + /** + * Allows configuration for Referrer Policy. + * + *

+ * Configuration is provided to the [ReferrerPolicyServerHttpHeadersWriter] which support the writing + * of the header as detailed in the W3C Technical Report: + *

+ *
    + *
  • Referrer-Policy
  • + *
+ * + * @param referrerPolicyConfig the customization to apply to the header + */ + fun referrerPolicy(referrerPolicyConfig: ServerReferrerPolicyDsl.() -> Unit) { + this.referrerPolicy = ServerReferrerPolicyDsl().apply(referrerPolicyConfig).get() + } + + /** + * Allows configuration for Feature + * Policy. + * + *

+ * Calling this method automatically enables (includes) the Feature-Policy + * header in the response using the supplied policy directive(s). + *

+ * + * @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() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt new file mode 100644 index 0000000000..2401b0c695 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.context.ReactorContextWebFilter +import org.springframework.security.web.server.context.ServerSecurityContextRepository + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] basic authorization using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save + * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the + * [ReactorContextWebFilter] must be configured to be able to load the value (they are not + * implicitly linked). + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to be + * populated on [BasicAuthenticationFilter] in the event that authentication fails. + */ +class ServerHttpBasicDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var securityContextRepository: ServerSecurityContextRepository? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + + private var disabled = false + + /** + * Disables HTTP basic authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HttpBasicSpec) -> Unit { + return { httpBasic -> + authenticationManager?.also { httpBasic.authenticationManager(authenticationManager) } + securityContextRepository?.also { httpBasic.securityContextRepository(securityContextRepository) } + authenticationEntryPoint?.also { httpBasic.authenticationEntryPoint(authenticationEntryPoint) } + if (disabled) { + httpBasic.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt new file mode 100644 index 0000000000..3f0fc3c76a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt @@ -0,0 +1,528 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl]. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * authorizeExchange { + * exchange("/public", permitAll) + * exchange(anyExchange, authenticated) + * } + * } + * } + * } + * ``` + * + * @author Eleftheria Stein + * @since 5.4 + * @param httpConfiguration the configurations to apply to [ServerHttpSecurity] + */ +operator fun ServerHttpSecurity.invoke(httpConfiguration: ServerHttpSecurityDsl.() -> Unit): SecurityWebFilterChain = + ServerHttpSecurityDsl(this, httpConfiguration).build() + +/** + * A [ServerHttpSecurity] Kotlin DSL created by [`http { }`][invoke] + * in order to configure [ServerHttpSecurity] using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @param init the configurations to apply to the provided [ServerHttpSecurity] + */ +class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val init: ServerHttpSecurityDsl.() -> Unit) { + + /** + * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the + * provided [ServerWebExchangeMatcher]. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) + * 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() + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt new file mode 100644 index 0000000000..135fded6f5 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.web.PortMapper +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] HTTPS redirection rules using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property portMapper the [PortMapper] that specifies a custom HTTPS port to redirect to. + */ +class ServerHttpsRedirectDsl { + var portMapper: PortMapper? = null + + private var redirectMatchers: Array? = 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) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt new file mode 100644 index 0000000000..47d3dbe349 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] logout support using idiomatic Kotlin + * code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property logoutHandler a [ServerLogoutHandler] that is invoked when logout occurs. + * @property logoutUrl the URL that triggers logout to occur. + * @property requiresLogout the [ServerWebExchangeMatcher] that triggers logout to occur. + * @property logoutSuccessHandler the [ServerLogoutSuccessHandler] to use after logout has + * occurred. + */ +class ServerLogoutDsl { + var logoutHandler: ServerLogoutHandler? = null + var logoutUrl: String? = null + var requiresLogout: ServerWebExchangeMatcher? = null + var logoutSuccessHandler: ServerLogoutSuccessHandler? = null + + private var disabled = false + + /** + * Disables logout + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.LogoutSpec) -> Unit { + return { logout -> + logoutHandler?.also { logout.logoutHandler(logoutHandler) } + logoutUrl?.also { logout.logoutUrl(logoutUrl) } + requiresLogout?.also { logout.requiresLogout(requiresLogout) } + logoutSuccessHandler?.also { logout.logoutSuccessHandler(logoutSuccessHandler) } + if (disabled) { + logout.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt new file mode 100644 index 0000000000..0aadac5b31 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] OAuth 2.0 client using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange] + * to an [Authentication]. + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s. + */ +class ServerOAuth2ClientDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var authenticationConverter: ServerAuthenticationConverter? = null + var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null + var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null + var authorizationRequestRepository: ServerAuthorizationRequestRepository? = 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) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt new file mode 100644 index 0000000000..ba257541c6 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save the [Authentication]. + * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after authentication success. + * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used after authentication failure. + * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange] + * to an [Authentication]. + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientService the service responsible for associating an access token to a client and resource + * owner. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s. + * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]s. + * @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an + * authentication request. + */ +class ServerOAuth2LoginDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var securityContextRepository: ServerSecurityContextRepository? = null + var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null + var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null + var authenticationConverter: ServerAuthenticationConverter? = null + var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null + var authorizedClientService: ReactiveOAuth2AuthorizedClientService? = null + var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null + var authorizationRequestRepository: ServerAuthorizationRequestRepository? = 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) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt new file mode 100644 index 0000000000..f09d4cba05 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver +import org.springframework.security.config.web.server.oauth2.resourceserver.ServerJwtDsl +import org.springframework.security.config.web.server.oauth2.resourceserver.ServerOpaqueTokenDsl +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 resource server using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use for requests authenticating with + * Bearer Tokens. + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use for requests authenticating with + * Bearer Tokens. + * @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with + * Bearer Tokens. + * @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use. + */ +class ServerOAuth2ResourceServerDsl { + var accessDeniedHandler: ServerAccessDeniedHandler? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var bearerTokenConverter: ServerAuthenticationConverter? = null + var authenticationManagerResolver: ReactiveAuthenticationManagerResolver? = 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) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt new file mode 100644 index 0000000000..7f0fc76a5d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.web.server.savedrequest.ServerRequestCache + +/** + * A Kotlin DSL to configure the request cache using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property requestCache allows explicit configuration of the [ServerRequestCache] to be used. + */ +class ServerRequestCacheDsl { + var requestCache: ServerRequestCache? = null + + private var disabled = false + + /** + * Disables the request cache. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.RequestCacheSpec) -> Unit { + return { requestCacheConfig -> + requestCache?.also { + requestCacheConfig.requestCache(requestCache) + if (disabled) { + requestCacheConfig.disable() + } + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt new file mode 100644 index 0000000000..8d6f885a09 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] X509 based pre authentication using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property principalExtractor the [X509PrincipalExtractor] used to obtain the principal for use within the framework. + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + */ +class ServerX509Dsl { + var principalExtractor: X509PrincipalExtractor? = null + var authenticationManager: ReactiveAuthenticationManager? = null + + internal fun get(): (ServerHttpSecurity.X509Spec) -> Unit { + return { x509 -> + authenticationManager?.also { x509.authenticationManager(authenticationManager) } + principalExtractor?.also { x509.principalExtractor(principalExtractor) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDsl.kt new file mode 100644 index 0000000000..519432021c --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDsl.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] cache control headers using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class ServerCacheControlDsl { + private var disabled = false + + /** + * Disables cache control response headers + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit { + return { cacheControl -> + if (disabled) { + cacheControl.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDsl.kt new file mode 100644 index 0000000000..003e8a98c1 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] Content-Security-Policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class ServerContentSecurityPolicyDsl { + var policyDirectives: String? = null + var reportOnly: Boolean? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit { + return { contentSecurityPolicy -> + policyDirectives?.also { + contentSecurityPolicy.policyDirectives(policyDirectives) + } + reportOnly?.also { + contentSecurityPolicy.reportOnly(reportOnly!!) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDsl.kt new file mode 100644 index 0000000000..4a3f4fc1f0 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDsl.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] the content type options header + * using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class ServerContentTypeOptionsDsl { + private var disabled = false + + /** + * Disables content type options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit { + return { contentTypeOptions -> + if (disabled) { + contentTypeOptions.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDsl.kt new file mode 100644 index 0000000000..767bdfe8ff --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDsl.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] X-Frame-Options header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property mode the X-Frame-Options mode to set in the response header. + */ +class ServerFrameOptionsDsl { + var mode: XFrameOptionsServerHttpHeadersWriter.Mode? = null + + private var disabled = false + + /** + * Disables the X-Frame-Options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit { + return { frameOptions -> + mode?.also { + frameOptions.mode(mode) + } + if (disabled) { + frameOptions.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDsl.kt new file mode 100644 index 0000000000..815ed23f42 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDsl.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity +import java.time.Duration + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] HTTP Strict Transport Security + * header using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property maxAge he value for the max-age directive of the Strict-Transport-Security + * header. + * @property includeSubdomains if true, subdomains should be considered HSTS Hosts too. + * @property preload if true, preload will be included in HSTS Header. + */ +class ServerHttpStrictTransportSecurityDsl { + var maxAge: Duration? = null + var includeSubdomains: Boolean? = null + var preload: Boolean? = null + + private var disabled = false + + /** + * Disables the X-Frame-Options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit { + return { hsts -> + maxAge?.also { hsts.maxAge(maxAge) } + includeSubdomains?.also { hsts.includeSubdomains(includeSubdomains!!) } + preload?.also { hsts.preload(preload!!) } + if (disabled) { + hsts.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDsl.kt new file mode 100644 index 0000000000..7e6ff46ce1 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDsl.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] referrer policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property policy the policy to be used in the response header. + */ +class ServerReferrerPolicyDsl { + var policy: ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit { + return { referrerPolicy -> + policy?.also { + referrerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDsl.kt new file mode 100644 index 0000000000..257ac3c3bc --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDsl.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.springframework.security.config.web.server.ServerHttpSecurity + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] XSS protection header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +class ServerXssProtectionDsl { + private var disabled = false + + /** + * Disables cache control response headers + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit { + return { xss -> + if (disabled) { + xss.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDsl.kt new file mode 100644 index 0000000000..fdb26dacf9 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDsl.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.oauth2.resourceserver + +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import reactor.core.publisher.Mono +import java.security.interfaces.RSAPublicKey + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] JWT Resource Server support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property jwtAuthenticationConverter the [Converter] to use for converting a [Jwt] into an + * [AbstractAuthenticationToken]. + * @property jwtDecoder the [ReactiveJwtDecoder] to use. + * @property publicKey configures a [ReactiveJwtDecoder] that leverages the provided [RSAPublicKey] + * @property jwkSetUri configures a [ReactiveJwtDecoder] using a + * JSON Web Key (JWK) 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>? = 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) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDsl.kt new file mode 100644 index 0000000000..de772ecf79 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDsl.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.oauth2.resourceserver + +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] Opaque Token Resource Server support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property introspectionUri the URI of the Introspection endpoint. + * @property introspector the [ReactiveOpaqueTokenIntrospector] to use. + */ +class ServerOpaqueTokenDsl { + private var _introspectionUri: String? = null + private var _introspector: ReactiveOpaqueTokenIntrospector? = null + private var clientCredentials: Pair? = 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) } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt new file mode 100644 index 0000000000..b39a0564e5 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import java.util.* + +/** + * Tests for [AuthorizeExchangeDsl] + * + * @author Eleftheria Stein + */ +class AuthorizeExchangeDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when secured by matcher then responds with unauthorized`() { + this.spring.register(MatcherAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MatcherAuthenticatedConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + } + + @Test + fun `request when allowed by matcher then responds with ok`() { + this.spring.register(MatcherPermitAllConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MatcherPermitAllConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, permitAll) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/") + fun path() { + } + } + } + + @Test + fun `request when secured by pattern then responds with unauthorized`() { + this.spring.register(PatternAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `request when allowed by pattern then responds with ok`() { + this.spring.register(PatternAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/public") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PatternAuthenticatedConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/public", permitAll) + authorize("/**", authenticated) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/public") + fun public() { + } + } + } + + @Test + fun `request when missing required role then responds with forbidden`() { + this.spring.register(HasRoleConfig::class.java).autowire() + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HasRoleConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasRole("ADMIN")) + } + httpBasic { } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt new file mode 100644 index 0000000000..24d13b1c86 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerAnonymousDsl] + * + * @author Eleftheria Stein + */ +class ServerAnonymousDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `authentication when anonymous enabled then is of type anonymous authentication`() { + this.spring.register(AnonymousConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/principal") + .exchange() + .expectStatus().isOk + .expectBody().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().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().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().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): Mono { + return principal + .map { it.keyHash } + .map { it.toString() } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt new file mode 100644 index 0000000000..90256ccb54 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.reactive.CorsConfigurationSource +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerCorsDsl] + * + * @author Eleftheria Stein + */ +class ServerCorsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when CORS configured using bean then Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsBeanConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().valueEquals("Access-Control-Allow-Origin", "*") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsBeanConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + cors { } + } + } + + @Bean + open fun corsConfigurationSource(): CorsConfigurationSource { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + source.registerCorsConfiguration("/**", corsConfiguration) + return source + } + } + + @Test + fun `request when CORS configured using source then Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsSourceConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().valueEquals("Access-Control-Allow-Origin", "*") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsSourceConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + source.registerCorsConfiguration("/**", corsConfiguration) + return http { + cors { + configurationSource = source + } + } + } + } + + @Test + fun `request when CORS disabled then no Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().doesNotExist("Access-Control-Allow-Origin") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + cors { + disable() + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt new file mode 100644 index 0000000000..08649c7bb8 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerCsrfDsl] + * + * @author Eleftheria Stein + */ +class ServerCsrfDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `post when CSRF protection enabled then requires CSRF token`() { + this.spring.register(CsrfConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { } + } + } + } + + @Test + fun `post when CSRF protection disabled then CSRF token is not required`() { + this.spring.register(CsrfDisabledConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + disable() + } + } + } + + @RestController + internal class TestController { + @PostMapping("/") + fun home() { + } + } + } + + @Test + fun `post when request matches CSRF matcher then CSRF token required`() { + this.spring.register(CsrfMatcherConfig::class.java).autowire() + + this.client.post() + .uri("/csrf") + .exchange() + .expectStatus().isForbidden + } + + @Test + fun `post when request does not match CSRF matcher then CSRF token is not required`() { + this.spring.register(CsrfMatcherConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + requireCsrfProtectionMatcher = PathPatternParserServerWebExchangeMatcher("/csrf") + } + } + } + + @RestController + internal class TestController { + @PostMapping("/") + fun home() { + } + + @PostMapping("/csrf") + fun csrf() { + } + } + } + + @Test + fun `csrf when custom access denied handler then handler used`() { + this.spring.register(CustomAccessDeniedHandlerConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + + Mockito.verify(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(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 + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt new file mode 100644 index 0000000000..22b5de4b44 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.assertj.core.api.Assertions +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpStatus +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint +import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.util.* + +/** + * Tests for [ServerExceptionHandlingDsl] + * + * @author Eleftheria Stein + */ +class ServerExceptionHandlingDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `unauthenticated request when custom entry point then directed to custom entry point`() { + this.spring.register(EntryPointConfig::class.java).autowire() + + val result = this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + Assertions.assertThat(result.responseHeaders.location).hasPath("/auth") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class EntryPointConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + exceptionHandling { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth") + } + } + } + } + + @Test + fun `unauthorized request when custom access denied handler then directed to custom access denied handler`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AccessDeniedHandlerConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasRole("ADMIN")) + } + httpBasic { } + exceptionHandling { + accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER) + } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt new file mode 100644 index 0000000000..23afe0b865 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt @@ -0,0 +1,325 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers +import org.springframework.test.web.reactive.server.FluxExchangeResult +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.function.BodyInserters + +/** + * Tests for [ServerFormLoginDsl] + * + * @author Eleftheria Stein + */ +class ServerFormLoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when form login enabled then redirects to default login page`() { + this.spring.register(DefaultFormLoginConfig::class.java, UserDetailsConfig::class.java).autowire() + + val result: FluxExchangeResult = 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 = 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 = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + this.client + .mutateWith(csrf()) + .post() + .uri("/login") + .body(BodyInserters.fromFormData(data)) + .exchange() + + verify(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 = 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 = 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 = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + this.client + .mutateWith(csrf()) + .post() + .uri("/login") + .body(BodyInserters.fromFormData(data)) + .exchange() + + verify(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) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt new file mode 100644 index 0000000000..6cebc2cbc3 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerHeadersDsl] + * + * @author Eleftheria Stein + */ +class ServerHeadersDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when default headers configured then default headers are in the response`() { + this.spring.register(DefaultHeadersConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class DefaultHeadersConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { } + } + } + } + + @Test + fun `request when headers disabled then no security headers are in the response`() { + this.spring.register(HeadersDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) + .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) + .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) + .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL) + .expectHeader().doesNotExist(HttpHeaders.EXPIRES) + .expectHeader().doesNotExist(HttpHeaders.PRAGMA) + .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HeadersDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + disable() + } + } + } + } + + @Test + fun `request when feature policy configured then feature policy header in response`() { + this.spring.register(FeaturePolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Feature-Policy", "geolocation 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FeaturePolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + featurePolicy("geolocation 'self'") + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt new file mode 100644 index 0000000000..4d4c926d17 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.BDDMockito.given +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.Authentication +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono +import java.util.* + +/** + * Tests for [ServerHttpBasicDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpBasicDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `http basic when no authorization header then responds with unauthorized`() { + this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `http basic when valid authorization header then responds with ok`() { + this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpBasicConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { } + } + } + + @RestController + internal class PathController { + @RequestMapping("/") + fun path() { + } + } + } + + @Test + fun `http basic when custom authentication manager then manager used`() { + given>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any())) + .willReturn(Mono.just(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(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(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(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) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt new file mode 100644 index 0000000000..0b6135741f --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerHttpSecurityDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpSecurityDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when it does not match the security matcher then the security rules do not apply`() { + this.spring.register(PatternMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @Test + fun `request when it matches the security matcher then the security rules apply`() { + this.spring.register(PatternMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/api") + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PatternMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + } + + @Test + fun `post when default security configured then CSRF prevents the request`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isForbidden + } + + @Test + fun `request when default security configured then default headers are in the response`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class DefaultSecurityConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt new file mode 100644 index 0000000000..9de8fc77d4 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.PortMapperImpl +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.util.* + +/** + * Tests for [ServerHttpsRedirectDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpsRedirectDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() { + this.spring.register(HttpRedirectMatcherConfig::class.java).autowire() + + val result = this.client.get() + .uri("/secure") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @Test + fun `request when does not match redirect to HTTPS matcher then does not redirect`() { + this.spring.register(HttpRedirectMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure")) + } + } + } + } + + @Test + fun `request when matches redirect to HTTPS function then redirects to HTTPS`() { + this.spring.register(HttpRedirectFunctionConfig::class.java).autowire() + + val result = this.client.get() + .uri("/") + .header("X-Requires-Https", "required") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @Test + fun `request when does not match redirect to HTTPS function then does not redirect`() { + this.spring.register(HttpRedirectFunctionConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectFunctionConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen { + it.request.headers.containsKey("X-Requires-Https") + } + } + } + } + } + + @Test + fun `request when multiple rules configured then only the last rule applies`() { + this.spring.register(HttpRedirectMatcherAndFunctionConfig::class.java).autowire() + + this.client.get() + .uri("/secure") + .exchange() + .expectStatus().isNotFound + + val result = this.client.get() + .uri("/") + .header("X-Requires-Https", "required") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectMatcherAndFunctionConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure")) + httpsRedirectWhen { + it.request.headers.containsKey("X-Requires-Https") + } + } + } + } + } + + @Test + fun `request when port mapper configured then redirected to HTTPS port`() { + this.spring.register(PortMapperConfig::class.java).autowire() + + val result = this.client.get() + .uri("http://localhost:543") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https").hasPort(123) + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PortMapperConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val customPortMapper = PortMapperImpl() + customPortMapper.setPortMappings(Collections.singletonMap("543", "123")) + return http { + redirectToHttps { + portMapper = customPortMapper + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt new file mode 100644 index 0000000000..4acdeea943 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerLogoutDsl] + * + * @author Eleftheria Stein + */ +class ServerLogoutDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `logout when defaults used then redirects to login page`() { + this.spring.register(LogoutConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class LogoutConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { } + } + } + } + + @Test + fun `logout when custom logout URL then custom URL redirects to login page`() { + this.spring.register(CustomUrlConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/custom-logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomUrlConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + logoutUrl = "/custom-logout" + } + } + } + } + + @Test + fun `logout when custom requires logout matcher then matching request redirects to login page`() { + this.spring.register(RequiresLogoutConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/custom-logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class RequiresLogoutConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + requiresLogout = PathPatternParserServerWebExchangeMatcher("/custom-logout") + } + } + } + } + + @Test + fun `logout when custom logout handler then custom handler invoked`() { + this.spring.register(CustomLogoutHandlerConfig::class.java).autowire() + + `when`(CustomLogoutHandlerConfig.LOGOUT_HANDLER.logout(any(), any())) + .thenReturn(Mono.empty()) + + this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + + verify(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(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() + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt new file mode 100644 index 0000000000..de317e59ee --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerOAuth2ClientDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2ClientDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `OAuth2 client when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ClientRepoConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Client { + clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `OAuth2 client when authorization request repository configured then custom repository used`() { + this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri { + it.path("/") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + } + .exchange() + + verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).loadAuthorizationRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthorizationRequestRepositoryConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + } + + @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 + 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 + 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() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt new file mode 100644 index 0000000000..43aae3aece --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerOAuth2LoginDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2LoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `oauth2Login when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ClientRepoConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { + clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `login page when OAuth2 login configured then default login page created`() { + this.spring.register(OAuth2LoginConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class OAuth2LoginConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + } + + @Test + fun `OAuth2 login when authorization request repository configured then custom repository used`() { + this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login/oauth2/code/google") + .exchange() + + verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).removeAuthorizationRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthorizationRequestRepositoryConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + } + + @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() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt new file mode 100644 index 0000000000..2ddfa2b107 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpStatus +import org.springframework.http.server.reactive.ServerHttpRequest +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint +import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.server.ServerWebExchange +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPublicKeySpec + +/** + * Tests for [ServerOAuth2ResourceServerDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2ResourceServerDslTests { + private val validJwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q" + + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when custom access denied handler configured then custom handler used`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AccessDeniedHandlerConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasAuthority("ADMIN")) + } + oauth2ResourceServer { + accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER) + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom entry point configured then custom entry point used`() { + this.spring.register(AuthenticationEntryPointConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationEntryPointConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.SEE_OTHER) + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom bearer token converter configured then custom converter used`() { + this.spring.register(BearerTokenConverterConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + + verify(BearerTokenConverterConfig.CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class BearerTokenConverterConfig { + companion object { + val CONVERTER: ServerBearerTokenAuthenticationConverter = mock(ServerBearerTokenAuthenticationConverter::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + bearerTokenConverter = CONVERTER + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom authentication manager resolver configured then custom resolver used`() { + this.spring.register(AuthenticationManagerResolverConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + + verify(AuthenticationManagerResolverConfig.RESOLVER).resolve(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationManagerResolverConfig { + companion object { + val RESOLVER: ReactiveAuthenticationManagerResolver = + mock(ReactiveAuthenticationManagerResolver::class.java) as ReactiveAuthenticationManagerResolver + } + + @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 + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt new file mode 100644 index 0000000000..338d10ebab --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.savedrequest.ServerRequestCache +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerRequestCacheDsl] + * + * @author Eleftheria Stein + */ +class ServerRequestCacheDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `GET when request cache enabled then redirected to cached page`() { + this.spring.register(RequestCacheConfig::class.java, UserDetailsConfig::class.java).autowire() + `when`(RequestCacheConfig.REQUEST_CACHE.removeMatchingRequest(any())).thenReturn(Mono.empty()) + + this.client.get() + .uri("/test") + .exchange() + + verify(RequestCacheConfig.REQUEST_CACHE).saveRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class RequestCacheConfig { + companion object { + var REQUEST_CACHE: ServerRequestCache = Mockito.mock(ServerRequestCache::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + requestCache { + requestCache = REQUEST_CACHE + } + } + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt new file mode 100644 index 0000000000..fa46c666da --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.ClassPathResource +import org.springframework.http.client.reactive.ClientHttpConnector +import org.springframework.http.server.reactive.ServerHttpRequestDecorator +import org.springframework.http.server.reactive.SslInfo +import org.springframework.lang.Nullable +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager +import org.springframework.test.web.reactive.server.MockServerConfigurer +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.WebTestClientConfigurer +import org.springframework.test.web.reactive.server.expectBody +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.ServerWebExchangeDecorator +import org.springframework.web.server.WebFilter +import org.springframework.web.server.WebFilterChain +import org.springframework.web.server.adapter.WebHttpHandlerBuilder +import reactor.core.publisher.Mono +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +/** + * Tests for [ServerX509Dsl] + * + * @author Eleftheria Stein + */ +class ServerX509DslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `x509 when configured with defaults then user authenticated with expected username`() { + this.spring + .register(X509DefaultConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java) + .autowire() + val certificate = loadCert("rod.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().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("rodatexampledotcom.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().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("rod.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().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 -> filters.add(0, filter) } + } + } + + private class SetSslInfoWebFilter(var certificate: X509Certificate) : WebFilter { + + override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { + 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 loadCert(location: String): T { + ClassPathResource(location).inputStream.use { inputStream -> + val certFactory = CertificateFactory.getInstance("X.509") + return certFactory.generateCertificate(inputStream) as T + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDslTests.kt new file mode 100644 index 0000000000..ac98f5c727 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerCacheControlDslTests.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerCacheControlDsl] + * + * @author Eleftheria Stein + */ +class ServerCacheControlDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when cache control configured then cache headers in response`() { + this.spring.register(CacheControlConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CacheControlConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + cache { } + } + } + } + } + + @Test + fun `request when cache control disabled then no cache headers in response`() { + this.spring.register(CacheControlDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL) + .expectHeader().doesNotExist(HttpHeaders.EXPIRES) + .expectHeader().doesNotExist(HttpHeaders.PRAGMA) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CacheControlDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + cache { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDslTests.kt new file mode 100644 index 0000000000..fcb0da15f7 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentSecurityPolicyDslTests.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerContentSecurityPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerContentSecurityPolicyDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when content security policy configured then content security policy header in response`() { + this.spring.register(ContentSecurityPolicyConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentSecurityPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { } + } + } + } + } + + @Test + fun `request when custom policy directives then custom policy directive in response header`() { + this.spring.register(CustomPolicyDirectivesConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'; script-src trustedscripts.example.com") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPolicyDirectivesConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { + policyDirectives = "default-src 'self'; script-src trustedscripts.example.com" + } + } + } + } + } + + @Test + fun `request when report only configured then content security policy report only header in response`() { + this.spring.register(ReportOnlyConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY, "default-src 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ReportOnlyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { + reportOnly = true + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDslTests.kt new file mode 100644 index 0000000000..c1dd051c44 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerContentTypeOptionsDslTests.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerContentTypeOptionsDsl] + * + * @author Eleftheria Stein + */ +class ServerContentTypeOptionsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when content type options configured then header in response`() { + this.spring.register(ContentTypeOptionsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentTypeOptionsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentTypeOptions { } + } + } + } + } + + @Test + fun `request when content type options disabled then no content type options header in response`() { + this.spring.register(ContentTypeOptionsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentTypeOptionsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentTypeOptions { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDslTests.kt new file mode 100644 index 0000000000..26fa2f0403 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerFrameOptionsDslTests.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerFrameOptionsDsl] + * + * @author Eleftheria Stein + */ +class ServerFrameOptionsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when frame options configured then header in response`() { + this.spring.register(FrameOptionsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FrameOptionsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { } + } + } + } + } + + @Test + fun `request when frame options disabled then no frame options header in response`() { + this.spring.register(FrameOptionsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FrameOptionsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { + disable() + } + } + } + } + } + + @Test + fun `request when frame options mode set then frame options response header has mode value`() { + this.spring.register(CustomModeConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN.name) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomModeConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { + mode = XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDslTests.kt new file mode 100644 index 0000000000..ccc6259293 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerHttpStrictTransportSecurityDslTests.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.time.Duration + +/** + * Tests for [ServerReferrerPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpStrictTransportSecurityDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when hsts configured then hsts header in response`() { + this.spring.register(HstsConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HstsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { } + } + } + } + } + + @Test + fun `request when hsts disabled then no hsts header in response`() { + this.spring.register(HstsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HstsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + disable() + } + } + } + } + } + + @Test + fun `request when max age set then max age in response header`() { + this.spring.register(MaxAgeConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=1 ; includeSubDomains") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MaxAgeConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + maxAge = Duration.ofSeconds(1) + } + } + } + } + } + + @Test + fun `request when includeSubdomains false then includeSubdomains not in response header`() { + this.spring.register(IncludeSubdomainsConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class IncludeSubdomainsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + includeSubdomains = false + } + } + } + } + } + + @Test + fun `request when preload true then preload included in response header`() { + this.spring.register(PreloadConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains ; preload") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PreloadConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + preload = true + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDslTests.kt new file mode 100644 index 0000000000..0cdcf8f7f4 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerReferrerPolicyDslTests.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerReferrerPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerReferrerPolicyDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when referrer policy configured then referrer policy header in response`() { + this.spring.register(ReferrerPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER.policy) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ReferrerPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + referrerPolicy { } + } + } + } + } + + @Test + fun `request when custom policy configured then custom policy in response header`() { + this.spring.register(CustomPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN.policy) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + referrerPolicy { + policy = ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDslTests.kt new file mode 100644 index 0000000000..556bcde3f5 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/headers/ServerXssProtectionDslTests.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerXssProtectionDsl] + * + * @author Eleftheria Stein + */ +class ServerXssProtectionDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when xss protection configured then xss header in response`() { + this.spring.register(XssConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class XssConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + xssProtection { } + } + } + } + } + + @Test + fun `request when xss protection disabled then no xss header in response`() { + this.spring.register(XssDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class XssDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + xssProtection { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDslTests.kt new file mode 100644 index 0000000000..d6d3623e8d --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerJwtDslTests.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.oauth2.resourceserver + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.core.convert.converter.Converter +import org.springframework.http.HttpHeaders +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPublicKeySpec +import javax.annotation.PreDestroy + +/** + * Tests for [ServerJwtDsl] + * + * @author Eleftheria Stein + */ +class ServerJwtDslTests { + + private val expired = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzUwMzc4OTd9.jqZDDjfc2eysX44lHXEIr9XFd2S8vjIZHCccZU-dRWMRJNsQ1QN5VNnJGklqJBXJR4qgla6cmVqPOLkUHDb0sL0nxM5XuzQaG5ZzKP81RV88shFyAiT0fD-6nl1k-Fai-Fu-VkzSpNXgeONoTxDaYhdB-yxmgrgsApgmbOTE_9AcMk-FQDXQ-pL9kynccFGV0lZx4CA7cyknKN7KBxUilfIycvXODwgKCjj_1WddLTCNGYogJJSg__7NoxzqbyWd3udbHVjqYq7GsMMrGB4_2kBD4CkghOSNcRHbT_DIXowxfAVT7PAg7Q0E5ruZsr2zPZacEUDhJ6-wbvlA0FAOUg" + private val messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q" + private val jwkSet = "{\n" + + " \"keys\":[\n" + + " {\n" + + " \"kty\":\"RSA\",\n" + + " \"e\":\"AQAB\",\n" + + " \"use\":\"sig\",\n" + + " \"kid\":\"one\",\n" + + " \"n\":\"0IUjrPZDz-3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh-64VhYUa-vz17zYCmuB8fFj4XHE3MLkWIG-AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn-85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp-Cln6JROJcZUIRJ-QvS6QONWeS2407uQmS-i-lybsqaH0ldYC7NBEBA5inPQ\"\n" + + " }\n" + + " ]\n" + + "}\n" + + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when JWT configured with public key and valid token then responds with ok`() { + this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) } + .exchange() + .expectStatus().isOk + } + + @Test + fun `request when JWT configured with public key and expired token then responds with unauthorized`() { + this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(expired) } + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PublicKeyConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `jwt when using custom JWT decoded then custom decoded used`() { + this.spring.register(CustomDecoderConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth("token") } + .exchange() + + verify(CustomDecoderConfig.JWT_DECODER).decode("token") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomDecoderConfig { + companion object { + var JWT_DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtDecoder = JWT_DECODER + } + } + } + } + } + + @Test + fun `jwt when using custom JWK Set URI then custom URI used`() { + this.spring.register(CustomJwkSetUriConfig::class.java).autowire() + + CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet)) + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) } + .exchange() + + val recordedRequest = CustomJwkSetUriConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/.well-known/jwks.json") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomJwkSetUriConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwkSetUri = mockWebServer().url("/.well-known/jwks.json").toString() + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } + + + @Test + fun `opaque token when custom JWT authentication converter then converter used`() { + this.spring.register(CustomJwtAuthenticationConverterConfig::class.java).autowire() + `when`(CustomJwtAuthenticationConverterConfig.DECODER.decode(anyString())).thenReturn( + Mono.just(Jwt.withTokenValue("token") + .header("alg", "none") + .claim(IdTokenClaimNames.SUB, "user") + .build())) + `when`(CustomJwtAuthenticationConverterConfig.CONVERTER.convert(any())) + .thenReturn(Mono.just(TestingAuthenticationToken("test", "this", "ROLE"))) + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth("token") } + .exchange() + + verify(CustomJwtAuthenticationConverterConfig.CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomJwtAuthenticationConverterConfig { + companion object { + var CONVERTER: Converter> = mock(Converter::class.java) as Converter> + 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 + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDslTests.kt new file mode 100644 index 0000000000..87521ae5ff --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/oauth2/resourceserver/ServerOpaqueTokenDslTests.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server.oauth2.resourceserver + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import javax.annotation.PreDestroy + +/** + * Tests for [ServerOpaqueTokenDsl] + * + * @author Eleftheria Stein + */ +class ServerOpaqueTokenDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `opaque token when using defaults then uses introspector bean`() { + this.spring.register(IntrospectorBeanConfig::class.java).autowire() + + IntrospectorBeanConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = IntrospectorBeanConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspect") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class IntrospectorBeanConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + + @Bean + open fun tokenIntrospectionClient(): ReactiveOpaqueTokenIntrospector { + return NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspect").toString(), "client", "secret") + } + } + + @Test + fun `opaque token when using custom introspector then introspector used`() { + this.spring.register(CustomIntrospectorConfig::class.java).autowire() + + CustomIntrospectorConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = CustomIntrospectorConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspector") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomIntrospectorConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspector").toString(), "client", "secret") + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } + + @Test + fun `opaque token when using custom introspection URI and credentials then custom used`() { + this.spring.register(CustomIntrospectionUriAndCredentialsConfig::class.java).autowire() + + CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspection-uri") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomIntrospectionUriAndCredentialsConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspectionUri = mockWebServer().url("/introspection-uri").toString() + introspectionClientCredentials("client", "secret") + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } +}