diff --git a/build.gradle b/build.gradle index ff9c247ff5..606a0042b8 100644 --- a/build.gradle +++ b/build.gradle @@ -4,15 +4,18 @@ buildscript { classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" classpath 'io.spring.nohttp:nohttp-gradle:0.0.2.RELEASE' classpath "io.freefair.gradle:aspectj-plugin:4.0.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } repositories { maven { url 'https://repo.spring.io/plugins-snapshot' } maven { url 'https://plugins.gradle.org/m2/' } } } + apply plugin: 'io.spring.nohttp' apply plugin: 'locks' apply plugin: 'io.spring.convention.root' +apply plugin: 'org.jetbrains.kotlin.jvm' group = 'org.springframework.security' description = 'Spring Security' diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 4acc4b332b..9dc5de6c8f 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -1,5 +1,6 @@ apply plugin: 'io.spring.convention.spring-module' apply plugin: 'trang' +apply plugin: 'kotlin' dependencies { // NB: Don't add other compile time dependencies to the config module as this breaks tooling @@ -27,6 +28,8 @@ dependencies { optional'org.springframework:spring-web' optional'org.springframework:spring-webflux' optional'org.springframework:spring-websocket' + optional 'org.jetbrains.kotlin:kotlin-reflect' + optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' provided 'javax.servlet:javax.servlet-api' @@ -84,4 +87,11 @@ rncToXsd { xslFile = new File(rncDir, 'spring-security.xsl') } +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = ["-Xjsr305=strict"] + } +} + build.dependsOn rncToXsd diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt new file mode 100644 index 0000000000..6aa2906530 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.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.servlet + +import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.security.web.util.matcher.RequestMatcher + +abstract class AbstractRequestMatcherDsl { + + /** + * Matches any request. + */ + val anyRequest: RequestMatcher = AnyRequestMatcher.INSTANCE + + protected data class MatcherAuthorizationRule(val matcher: RequestMatcher, + override val rule: String) : AuthorizationRule(rule) + + protected data class PatternAuthorizationRule(val pattern: String, + val patternType: PatternType, + val servletPath: String?, + override val rule: String) : AuthorizationRule(rule) + + protected abstract class AuthorizationRule(open val rule: String) + + protected enum class PatternType { + ANT, MVC + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AnonymousDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AnonymousDsl.kt new file mode 100644 index 0000000000..8190399ba4 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AnonymousDsl.kt @@ -0,0 +1,68 @@ +/* + * 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.servlet + +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter + +/** + * A Kotlin DSL to configure [HttpSecurity] anonymous authentication using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @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 authenticationProvider the [AuthenticationProvider] used to validate an + * anonymous user + * @property authenticationFilter the [AnonymousAuthenticationFilter] used to populate + * an anonymous user. + */ +class AnonymousDsl { + var key: String? = null + var principal: Any? = null + var authorities: List? = null + var authenticationProvider: AuthenticationProvider? = null + var authenticationFilter: AnonymousAuthenticationFilter? = null + + private var disabled = false + + /** + * Disable anonymous authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (AnonymousConfigurer) -> Unit { + return { anonymous -> + key?.also { anonymous.key(key) } + principal?.also { anonymous.principal(principal) } + authorities?.also { anonymous.authorities(authorities) } + authenticationProvider?.also { anonymous.authenticationProvider(authenticationProvider) } + authenticationFilter?.also { anonymous.authenticationFilter(authenticationFilter) } + if (disabled) { + anonymous.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt new file mode 100644 index 0000000000..a8b42aae50 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt @@ -0,0 +1,159 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer +import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.util.ClassUtils + +/** + * A Kotlin DSL to configure [HttpSecurity] request authorization using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { + private val authorizationRules = mutableListOf() + + private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" + private val MVC_PRESENT = ClassUtils.isPresent( + HANDLER_MAPPING_INTROSPECTOR, + AuthorizeRequestsDsl::class.java.classLoader) + + /** + * Adds a request authorization rule. + * + * @param matches the [RequestMatcher] to match incoming requests against + * @param access the SpEL expression to secure the matching request + * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") + */ + fun authorize(matches: RequestMatcher = AnyRequestMatcher.INSTANCE, + access: String = "authenticated") { + authorizationRules.add(MatcherAuthorizationRule(matches, access)) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param access the SpEL expression to secure the matching request + * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") + */ + fun authorize(pattern: String, access: String = "authenticated") { + if (MVC_PRESENT) { + authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, null, access)) + } else { + authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, null, access)) + } + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param servletPath the servlet path to match incoming requests against. This + * only applies when using an MVC pattern matcher. + * @param access the SpEL expression to secure the matching request + * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") + */ + fun authorize(pattern: String, servletPath: String, access: String = "authenticated") { + if (MVC_PRESENT) { + authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, servletPath, access)) + } else { + authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, servletPath, access)) + } + } + + /** + * Specify that URLs require a particular authority. + * + * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc). + * @return the SpEL expression "hasAuthority" with the given authority as a + * parameter + */ + fun hasAuthority(authority: String) = "hasAuthority('$authority')" + + /** + * Specify that URLs are allowed by anyone. + */ + val permitAll = "permitAll" + + /** + * Specify that URLs are allowed by anonymous users. + */ + val anonymous = "anonymous" + + /** + * Specify that URLs are allowed by users that have been remembered. + */ + val rememberMe = "rememberMe" + + /** + * Specify that URLs are not allowed by anyone. + */ + val denyAll = "denyAll" + + /** + * Specify that URLs are allowed by any authenticated user. + */ + val authenticated = "authenticated" + + /** + * Specify that URLs are allowed by users who have authenticated and were not + * "remembered". + */ + val fullyAuthenticated = "fullyAuthenticated" + + internal fun get(): (ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry) -> Unit { + return { requests -> + authorizationRules.forEach { rule -> + when (rule) { + is MatcherAuthorizationRule -> requests.requestMatchers(rule.matcher).access(rule.rule) + is PatternAuthorizationRule -> { + when (rule.patternType) { + PatternType.ANT -> requests.antMatchers(rule.pattern).access(rule.rule) + PatternType.MVC -> { + val mvcMatchersAuthorizeUrl = requests.mvcMatchers(rule.pattern) + rule.servletPath?.also { mvcMatchersAuthorizeUrl.servletPath(rule.servletPath) } + mvcMatchersAuthorizeUrl.access(rule.rule) + } + } + } + } + } + } + } +} + diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/CorsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CorsDsl.kt new file mode 100644 index 0000000000..f7f99a78fa --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CorsDsl.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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.CorsConfigurer + +/** + * A Kotlin DSL to configure [HttpSecurity] CORS using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class CorsDsl { + private var disabled = false + + /** + * Disable CORS. + */ + fun disable() { + disabled = true + } + + internal fun get(): (CorsConfigurer) -> Unit { + return { cors -> + if (disabled) { + cors.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt new file mode 100644 index 0000000000..abbbc22f54 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt @@ -0,0 +1,87 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy +import org.springframework.security.web.csrf.CsrfTokenRepository +import org.springframework.security.web.util.matcher.RequestMatcher +import javax.servlet.http.HttpServletRequest + +/** + * A Kotlin DSL to configure [HttpSecurity] CSRF protection + * using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property csrfTokenRepository the [CsrfTokenRepository] to use. + * @property requireCsrfProtectionMatcher specify the [RequestMatcher] to use for + * determining when CSRF should be applied. + * @property sessionAuthenticationStrategy the [SessionAuthenticationStrategy] to use. + */ +class CsrfDsl { + var csrfTokenRepository: CsrfTokenRepository? = null + var requireCsrfProtectionMatcher: RequestMatcher? = null + var sessionAuthenticationStrategy: SessionAuthenticationStrategy? = null + + private var ignoringAntMatchers: Array? = null + private var ignoringRequestMatchers: Array? = null + private var disabled = false + + /** + * Allows specifying [HttpServletRequest]s that should not use CSRF Protection + * even if they match the [requireCsrfProtectionMatcher]. + * + * @param antMatchers the ANT pattern matchers that should not use CSRF + * protection + */ + fun ignoringAntMatchers(vararg antMatchers: String) { + ignoringAntMatchers = antMatchers + } + + /** + * Allows specifying [HttpServletRequest]s that should not use CSRF Protection + * even if they match the [requireCsrfProtectionMatcher]. + * + * @param requestMatchers the request matchers that should not use CSRF + * protection + */ + fun ignoringRequestMatchers(vararg requestMatchers: RequestMatcher) { + ignoringRequestMatchers = requestMatchers + } + + /** + * Disable CSRF protection + */ + fun disable() { + disabled = true + } + + internal fun get(): (CsrfConfigurer) -> Unit { + return { csrf -> + csrfTokenRepository?.also { csrf.csrfTokenRepository(csrfTokenRepository) } + requireCsrfProtectionMatcher?.also { csrf.requireCsrfProtectionMatcher(requireCsrfProtectionMatcher) } + sessionAuthenticationStrategy?.also { csrf.sessionAuthenticationStrategy(sessionAuthenticationStrategy) } + ignoringAntMatchers?.also { csrf.ignoringAntMatchers(*ignoringAntMatchers!!) } + ignoringRequestMatchers?.also { csrf.ignoringRequestMatchers(*ignoringRequestMatchers!!) } + if (disabled) { + csrf.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDsl.kt new file mode 100644 index 0000000000..e419a63bc3 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDsl.kt @@ -0,0 +1,92 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.security.web.util.matcher.RequestMatcher +import java.util.* + +/** + * A Kotlin DSL to configure [HttpSecurity] exception handling using idiomatic Kotlin + * code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property accessDeniedPage the URL to the access denied page + * @property accessDeniedHandler the [AccessDeniedHandler] to use + * @property authenticationEntryPoint the [AuthenticationEntryPoint] to use + */ +class ExceptionHandlingDsl { + var accessDeniedPage: String? = null + var accessDeniedHandler: AccessDeniedHandler? = null + var authenticationEntryPoint: AuthenticationEntryPoint? = null + + private var defaultDeniedHandlerMappings: LinkedHashMap = linkedMapOf() + private var defaultEntryPointMappings: LinkedHashMap = linkedMapOf() + private var disabled = false + + /** + * Sets a default [AccessDeniedHandler] to be used which prefers being + * invoked for the provided [RequestMatcher]. + * + * @param deniedHandler the [AccessDeniedHandler] to use + * @param preferredMatcher the [RequestMatcher] for this default + * [AccessDeniedHandler] + */ + fun defaultAccessDeniedHandlerFor(deniedHandler: AccessDeniedHandler, preferredMatcher: RequestMatcher) { + defaultDeniedHandlerMappings[preferredMatcher] = deniedHandler + } + + /** + * Sets a default [AuthenticationEntryPoint] to be used which prefers being + * invoked for the provided [RequestMatcher]. + * + * @param entryPoint the [AuthenticationEntryPoint] to use + * @param preferredMatcher the [RequestMatcher] for this default + * [AccessDeniedHandler] + */ + fun defaultAuthenticationEntryPointFor(entryPoint: AuthenticationEntryPoint, preferredMatcher: RequestMatcher) { + defaultEntryPointMappings[preferredMatcher] = entryPoint + } + + /** + * Disable exception handling. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ExceptionHandlingConfigurer) -> Unit { + return { exceptionHandling -> + accessDeniedPage?.also { exceptionHandling.accessDeniedPage(accessDeniedPage) } + accessDeniedHandler?.also { exceptionHandling.accessDeniedHandler(accessDeniedHandler) } + authenticationEntryPoint?.also { exceptionHandling.authenticationEntryPoint(authenticationEntryPoint) } + defaultDeniedHandlerMappings.forEach { (matcher, handler) -> + exceptionHandling.defaultAccessDeniedHandlerFor(handler, matcher) + } + defaultEntryPointMappings.forEach { (matcher, entryPoint) -> + exceptionHandling.defaultAuthenticationEntryPointFor(entryPoint, matcher) + } + if (disabled) { + exceptionHandling.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/FormLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/FormLoginDsl.kt new file mode 100644 index 0000000000..a935a7a5af --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/FormLoginDsl.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.servlet + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.AuthenticationSuccessHandler + +/** + * A Kotlin DSL to configure [HttpSecurity] form login using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property loginPage the login page to redirect to if authentication is required (i.e. + * "/login") + * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] used after + * authentication success + * @property authenticationFailureHandler the [AuthenticationFailureHandler] used after + * authentication success + * @property failureUrl the URL to send users if authentication fails + * @property loginProcessingUrl the URL to validate the credentials + * @property permitAll whether to grant access to the urls for [failureUrl] as well as + * for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user + */ +class FormLoginDsl { + var loginPage: String? = null + var authenticationSuccessHandler: AuthenticationSuccessHandler? = null + var authenticationFailureHandler: AuthenticationFailureHandler? = null + var failureUrl: String? = null + var loginProcessingUrl: String? = null + var permitAll: Boolean? = null + + private var defaultSuccessUrlOption: Pair? = null + + /** + * Grants access to the urls for [failureUrl] as well as for the [HttpSecurityBuilder], the + * [loginPage] and [loginProcessingUrl] for every user. + */ + fun permitAll() { + permitAll = true + } + + /** + * Specifies where users will be redirected after authenticating successfully if + * they have not visited a secured page prior to authenticating or [alwaysUse] + * is true. + * + * @param defaultSuccessUrl the default success url + * @param alwaysUse true if the [defaultSuccessUrl] should be used after + * authentication despite if a protected page had been previously visited + */ + fun defaultSuccessUrl(defaultSuccessUrl: String, alwaysUse: Boolean) { + defaultSuccessUrlOption = Pair(defaultSuccessUrl, alwaysUse) + } + + internal fun get(): (FormLoginConfigurer) -> Unit { + return { login -> + loginPage?.also { login.loginPage(loginPage) } + failureUrl?.also { login.failureUrl(failureUrl) } + loginProcessingUrl?.also { login.loginProcessingUrl(loginProcessingUrl) } + permitAll?.also { login.permitAll(permitAll!!) } + defaultSuccessUrlOption?.also { + login.defaultSuccessUrl(defaultSuccessUrlOption!!.first, defaultSuccessUrlOption!!.second) + } + authenticationSuccessHandler?.also { login.successHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { login.failureHandler(authenticationFailureHandler) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt new file mode 100644 index 0000000000..8542a116ee --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.config.web.servlet.headers.* +import org.springframework.security.web.header.writers.* +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter + +/** + * A Kotlin DSL to configure [HttpSecurity] headers using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property defaultsDisabled whether all of the default headers should be included in the response + */ +class HeadersDsl { + private var contentTypeOptions: ((HeadersConfigurer.ContentTypeOptionsConfig) -> Unit)? = null + private var xssProtection: ((HeadersConfigurer.XXssConfig) -> Unit)? = null + private var cacheControl: ((HeadersConfigurer.CacheControlConfig) -> Unit)? = null + private var hsts: ((HeadersConfigurer.HstsConfig) -> Unit)? = null + private var frameOptions: ((HeadersConfigurer.FrameOptionsConfig) -> Unit)? = null + private var hpkp: ((HeadersConfigurer.HpkpConfig) -> Unit)? = null + private var contentSecurityPolicy: ((HeadersConfigurer.ContentSecurityPolicyConfig) -> Unit)? = null + private var referrerPolicy: ((HeadersConfigurer.ReferrerPolicyConfig) -> Unit)? = null + private var featurePolicyDirectives: String? = null + + var defaultsDisabled: Boolean? = null + + /** + * Configures the [XContentTypeOptionsHeaderWriter] which inserts the X-Content-Type-Options header + * + * @param contentTypeOptionsConfig the customization to apply to the header + */ + fun contentTypeOptions(contentTypeOptionsConfig: ContentTypeOptionsDsl.() -> Unit) { + this.contentTypeOptions = ContentTypeOptionsDsl().apply(contentTypeOptionsConfig).get() + } + + /** + * Note this is not comprehensive XSS protection! + * + *

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

+ * + * @param xssProtectionConfig the customization to apply to the header + */ + fun xssProtection(xssProtectionConfig: XssProtectionConfigDsl.() -> Unit) { + this.xssProtection = XssProtectionConfigDsl().apply(xssProtectionConfig).get() + } + + /** + * Allows customizing the [CacheControlHeadersWriter]. 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 header + */ + fun cacheControl(cacheControlConfig: CacheControlDsl.() -> Unit) { + this.cacheControl = CacheControlDsl().apply(cacheControlConfig).get() + } + + /** + * Allows customizing the [HstsHeaderWriter] which provides support for HTTP Strict Transport Security + * (HSTS). + * + * @param hstsConfig the customization to apply to the header + */ + fun httpStrictTransportSecurity(hstsConfig: HttpStrictTransportSecurityDsl.() -> Unit) { + this.hsts = HttpStrictTransportSecurityDsl().apply(hstsConfig).get() + } + + /** + * Allows customizing the [XFrameOptionsHeaderWriter] which add the X-Frame-Options + * header. + * + * @param frameOptionsConfig the customization to apply to the header + */ + fun frameOptions(frameOptionsConfig: FrameOptionsDsl.() -> Unit) { + this.frameOptions = FrameOptionsDsl().apply(frameOptionsConfig).get() + } + + /** + * Allows customizing the [HpkpHeaderWriter] which provides support for HTTP Public Key Pinning (HPKP). + * + * @param hpkpConfig the customization to apply to the header + */ + fun httpPublicKeyPinning(hpkpConfig: HttpPublicKeyPinningDsl.() -> Unit) { + this.hpkp = HttpPublicKeyPinningDsl().apply(hpkpConfig).get() + } + + /** + * Allows configuration for Content Security Policy (CSP) Level 2. + * + *

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

+ * + * @param contentSecurityPolicyConfig the customization to apply to the header + */ + fun contentSecurityPolicy(contentSecurityPolicyConfig: ContentSecurityPolicyDsl.() -> Unit) { + this.contentSecurityPolicy = ContentSecurityPolicyDsl().apply(contentSecurityPolicyConfig).get() + } + + /** + * Allows configuration for Referrer Policy. + * + *

+ * Configuration is provided to the [ReferrerPolicyHeaderWriter] 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: ReferrerPolicyDsl.() -> Unit) { + this.referrerPolicy = ReferrerPolicyDsl().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 + } + + internal fun get(): (HeadersConfigurer) -> Unit { + return { headers -> + defaultsDisabled?.also { + if (defaultsDisabled!!) { + headers.defaultsDisabled() + } + } + contentTypeOptions?.also { + headers.contentTypeOptions(contentTypeOptions) + } + xssProtection?.also { + headers.xssProtection(xssProtection) + } + cacheControl?.also { + headers.cacheControl(cacheControl) + } + hsts?.also { + headers.httpStrictTransportSecurity(hsts) + } + frameOptions?.also { + headers.frameOptions(frameOptions) + } + hpkp?.also { + headers.httpPublicKeyPinning(hpkp) + } + contentSecurityPolicy?.also { + headers.contentSecurityPolicy(contentSecurityPolicy) + } + referrerPolicy?.also { + headers.referrerPolicy(referrerPolicy) + } + featurePolicyDirectives?.also { + headers.featurePolicy(featurePolicyDirectives) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpBasicDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpBasicDsl.kt new file mode 100644 index 0000000000..8ecd076d82 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpBasicDsl.kt @@ -0,0 +1,62 @@ +/* + * 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.servlet + +import org.springframework.security.authentication.AuthenticationDetailsSource +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import javax.servlet.http.HttpServletRequest + +/** + * A Kotlin DSL to configure [HttpSecurity] basic authentication using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property realmName the HTTP Basic realm to use. If [authenticationEntryPoint] + * has been invoked, invoking this method will result in an error. + * @property authenticationEntryPoint the [AuthenticationEntryPoint] to be populated on + * [BasicAuthenticationFilter] in the event that authentication fails. + * @property authenticationDetailsSource the custom [AuthenticationDetailsSource] to use for + * basic authentication. + */ +class HttpBasicDsl { + var realmName: String? = null + var authenticationEntryPoint: AuthenticationEntryPoint? = null + var authenticationDetailsSource: AuthenticationDetailsSource? = null + + private var disabled = false + + /** + * Disables HTTP basic authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (HttpBasicConfigurer) -> Unit { + return { httpBasic -> + realmName?.also { httpBasic.realmName(realmName) } + authenticationEntryPoint?.also { httpBasic.authenticationEntryPoint(authenticationEntryPoint) } + authenticationDetailsSource?.also { httpBasic.authenticationDetailsSource(authenticationDetailsSource) } + if (disabled) { + httpBasic.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt new file mode 100644 index 0000000000..909ffb7fa6 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt @@ -0,0 +1,651 @@ +/* + * 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.servlet + +import org.springframework.context.ApplicationContext +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.util.ClassUtils +import javax.servlet.http.HttpServletRequest + +/** + * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl]. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * authorizeRequests { + * request("/public", permitAll) + * request(anyRequest, authenticated) + * } + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @author Eleftheria Stein + * @since 5.3 + * @param httpConfiguration the configurations to apply to [HttpSecurity] + */ +operator fun HttpSecurity.invoke(httpConfiguration: HttpSecurityDsl.() -> Unit) = + HttpSecurityDsl(this, httpConfiguration).build() + +/** + * An [HttpSecurity] Kotlin DSL created by [`http { }`][invoke] + * in order to configure [HttpSecurity] using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @param http the [HttpSecurity] which all configurations will be applied to + * @param init the configurations to apply to the provided [HttpSecurity] + */ +class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecurityDsl.() -> Unit) { + private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" + + /** + * Allows configuring the [HttpSecurity] to only be invoked when matching the + * provided pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * securityMatcher("/private/**") + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @param pattern one or more patterns used to determine whether this + * configuration should be invoked. + */ + fun securityMatcher(vararg pattern: String) { + val mvcPresent = ClassUtils.isPresent( + HANDLER_MAPPING_INTROSPECTOR, + AuthorizeRequestsDsl::class.java.classLoader) + this.http.requestMatchers { + if (mvcPresent) { + it.mvcMatchers(*pattern) + } else { + it.antMatchers(*pattern) + } + } + } + + /** + * Allows configuring the [HttpSecurity] to only be invoked when matching the + * provided [RequestMatcher]. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * securityMatcher(AntPathRequestMatcher("/private/**")) + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @param requestMatcher one or more [RequestMatcher] used to determine whether + * this configuration should be invoked. + */ + fun securityMatcher(vararg requestMatcher: RequestMatcher) { + this.http.requestMatchers { + it.requestMatchers(*requestMatcher) + } + } + + /** + * Enables form based authentication. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @param formLoginConfiguration custom configurations to be applied + * to the form based authentication + * @see [FormLoginDsl] + */ + fun formLogin(formLoginConfiguration: FormLoginDsl.() -> Unit) { + val loginCustomizer = FormLoginDsl().apply(formLoginConfiguration).get() + this.http.formLogin(loginCustomizer) + } + + /** + * Allows restricting access based upon the [HttpServletRequest] + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * authorizeRequests { + * request("/public", permitAll) + * request(anyRequest, authenticated) + * } + * } + * } + * } + * ``` + * + * @param authorizeRequestsConfiguration custom configuration that specifies + * access for requests + * @see [AuthorizeRequestsDsl] + */ + fun authorizeRequests(authorizeRequestsConfiguration: AuthorizeRequestsDsl.() -> Unit) { + val authorizeRequestsCustomizer = AuthorizeRequestsDsl().apply(authorizeRequestsConfiguration).get() + this.http.authorizeRequests(authorizeRequestsCustomizer) + } + + /** + * Enables HTTP basic authentication. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * httpBasic { + * realmName = "Custom Realm" + * } + * } + * } + * } + * ``` + * + * @param httpBasicConfiguration custom configurations to be applied to the + * HTTP basic authentication + * @see [HttpBasicDsl] + */ + fun httpBasic(httpBasicConfiguration: HttpBasicDsl.() -> Unit) { + val httpBasicCustomizer = HttpBasicDsl().apply(httpBasicConfiguration).get() + this.http.httpBasic(httpBasicCustomizer) + } + + /** + * Allows configuring response headers. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * headers { + * referrerPolicy { + * policy = ReferrerPolicy.SAME_ORIGIN + * } + * } + * } + * } + * } + * ``` + * + * @param headersConfiguration custom configurations to configure the + * response headers + * @see [HeadersDsl] + */ + fun headers(headersConfiguration: HeadersDsl.() -> Unit) { + val headersCustomizer = HeadersDsl().apply(headersConfiguration).get() + this.http.headers(headersCustomizer) + } + + /** + * Enables CORS. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * cors { + * disable() + * } + * } + * } + * } + * ``` + * + * @param corsConfiguration custom configurations to configure the + * response headers + * @see [CorsDsl] + */ + fun cors(corsConfiguration: CorsDsl.() -> Unit) { + val corsCustomizer = CorsDsl().apply(corsConfiguration).get() + this.http.cors(corsCustomizer) + } + + /** + * Allows configuring session management. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * sessionManagement { + * invalidSessionUrl = "/invalid-session" + * sessionConcurrency { + * maximumSessions = 1 + * } + * } + * } + * } + * } + * ``` + * + * @param sessionManagementConfiguration custom configurations to configure + * session management + * @see [SessionManagementDsl] + */ + fun sessionManagement(sessionManagementConfiguration: SessionManagementDsl.() -> Unit) { + val sessionManagementCustomizer = SessionManagementDsl().apply(sessionManagementConfiguration).get() + this.http.sessionManagement(sessionManagementCustomizer) + } + + /** + * Allows configuring a port mapper. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * portMapper { + * map(80, 443) + * } + * } + * } + * } + * ``` + * + * @param portMapperConfiguration custom configurations to configure + * the port mapper + * @see [PortMapperDsl] + */ + fun portMapper(portMapperConfiguration: PortMapperDsl.() -> Unit) { + val portMapperCustomizer = PortMapperDsl().apply(portMapperConfiguration).get() + this.http.portMapper(portMapperCustomizer) + } + + /** + * Allows configuring channel security based upon the [HttpServletRequest] + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * requiresChannel { + * secure("/public", requiresInsecure) + * secure(anyRequest, requiresSecure) + * } + * } + * } + * } + * ``` + * + * @param requiresChannelConfiguration custom configuration that specifies + * channel security + * @see [RequiresChannelDsl] + */ + fun requiresChannel(requiresChannelConfiguration: RequiresChannelDsl.() -> Unit) { + val requiresChannelCustomizer = RequiresChannelDsl().apply(requiresChannelConfiguration).get() + this.http.requiresChannel(requiresChannelCustomizer) + } + + /** + * Adds X509 based pre authentication to an application + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * x509 { } + * } + * } + * } + * ``` + * + * @param x509Configuration custom configuration to apply to the + * X509 based pre authentication + * @see [X509Dsl] + */ + fun x509(x509Configuration: X509Dsl.() -> Unit) { + val x509Customizer = X509Dsl().apply(x509Configuration).get() + this.http.x509(x509Customizer) + } + + /** + * Enables request caching. Specifically this ensures that requests that + * are saved (i.e. after authentication is required) are later replayed. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * requestCache { } + * } + * } + * } + * ``` + * + * @param requestCacheConfiguration custom configuration to apply to the + * request cache + * @see [RequestCacheDsl] + */ + fun requestCache(requestCacheConfiguration: RequestCacheDsl.() -> Unit) { + val requestCacheCustomizer = RequestCacheDsl().apply(requestCacheConfiguration).get() + this.http.requestCache(requestCacheCustomizer) + } + + /** + * Allows configuring exception handling. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * exceptionHandling { + * accessDeniedPage = "/access-denied" + * } + * } + * } + * } + * ``` + * + * @param exceptionHandlingConfiguration custom configuration to apply to the + * exception handling + * @see [ExceptionHandlingDsl] + */ + fun exceptionHandling(exceptionHandlingConfiguration: ExceptionHandlingDsl.() -> Unit) { + val exceptionHandlingCustomizer = ExceptionHandlingDsl().apply(exceptionHandlingConfiguration).get() + this.http.exceptionHandling(exceptionHandlingCustomizer) + } + + /** + * Enables CSRF protection. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * csrf { } + * } + * } + * } + * ``` + * + * @param csrfConfiguration custom configuration to apply to CSRF + * @see [CsrfDsl] + */ + fun csrf(csrfConfiguration: CsrfDsl.() -> Unit) { + val csrfCustomizer = CsrfDsl().apply(csrfConfiguration).get() + this.http.csrf(csrfCustomizer) + } + + /** + * Provides logout support. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * logout { + * logoutUrl = "/log-out" + * } + * } + * } + * } + * ``` + * + * @param logoutConfiguration custom configuration to apply to logout + * @see [LogoutDsl] + */ + fun logout(logoutConfiguration: LogoutDsl.() -> Unit) { + val logoutCustomizer = LogoutDsl().apply(logoutConfiguration).get() + this.http.logout(logoutCustomizer) + } + + /** + * Configures authentication support using a SAML 2.0 Service Provider. + * A [RelyingPartyRegistrationRepository] is required and must be registered with + * the [ApplicationContext] or configured via + * [Saml2Dsl.relyingPartyRegistrationRepository] + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * saml2Login { + * relyingPartyRegistration = getSaml2RelyingPartyRegistration() + * } + * } + * } + * } + * ``` + * + * @param saml2LoginConfiguration custom configuration to configure the + * SAML2 service provider + * @see [Saml2Dsl] + */ + fun saml2Login(saml2LoginConfiguration: Saml2Dsl.() -> Unit) { + val saml2LoginCustomizer = Saml2Dsl().apply(saml2LoginConfiguration).get() + this.http.saml2Login(saml2LoginCustomizer) + } + + /** + * Allows configuring how an anonymous user is represented. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * anonymous { + * authorities = listOf(SimpleGrantedAuthority("ROLE_ANON")) + * } + * } + * } + * } + * ``` + * + * @param anonymousConfiguration custom configuration to configure the + * anonymous user + * @see [AnonymousDsl] + */ + fun anonymous(anonymousConfiguration: AnonymousDsl.() -> Unit) { + val anonymousCustomizer = AnonymousDsl().apply(anonymousConfiguration).get() + this.http.anonymous(anonymousCustomizer) + } + + /** + * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + * A [ClientRegistrationRepository] is required and must be registered as a Bean or + * configured via [OAuth2LoginDsl.clientRegistrationRepository] + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * oauth2Login { + * clientRegistrationRepository = getClientRegistrationRepository() + * } + * } + * } + * } + * ``` + * + * @param oauth2LoginConfiguration custom configuration to configure the + * OAuth 2.0 Login + * @see [OAuth2LoginDsl] + */ + fun oauth2Login(oauth2LoginConfiguration: OAuth2LoginDsl.() -> Unit) { + val oauth2LoginCustomizer = OAuth2LoginDsl().apply(oauth2LoginConfiguration).get() + this.http.oauth2Login(oauth2LoginCustomizer) + } + + /** + * Configures OAuth 2.0 client support. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * oauth2Client { } + * } + * } + * } + * ``` + * + * @param oauth2ClientConfiguration custom configuration to configure the + * OAuth 2.0 client support + * @see [OAuth2ClientDsl] + */ + fun oauth2Client(oauth2ClientConfiguration: OAuth2ClientDsl.() -> Unit) { + val oauth2ClientCustomizer = OAuth2ClientDsl().apply(oauth2ClientConfiguration).get() + this.http.oauth2Client(oauth2ClientCustomizer) + } + + /** + * Configures OAuth 2.0 resource server support. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * oauth2ResourceServer { + * jwt { } + * } + * } + * } + * } + * ``` + * + * @param oauth2ResourceServerConfiguration custom configuration to configure the + * OAuth 2.0 resource server support + * @see [OAuth2ResourceServerDsl] + */ + fun oauth2ResourceServer(oauth2ResourceServerConfiguration: OAuth2ResourceServerDsl.() -> Unit) { + val oauth2ResourceServerCustomizer = OAuth2ResourceServerDsl().apply(oauth2ResourceServerConfiguration).get() + this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer) + } + + /** + * Apply all configurations to the provided [HttpSecurity] + */ + internal fun build() { + init() + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/LogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/LogoutDsl.kt new file mode 100644 index 0000000000..5e4c6e9a47 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/LogoutDsl.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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer +import org.springframework.security.core.Authentication +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.security.web.authentication.logout.LogoutHandler +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler +import org.springframework.security.web.util.matcher.RequestMatcher +import java.util.* +import javax.servlet.http.HttpSession + +/** + * A Kotlin DSL to configure [HttpSecurity] logout support + * using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property clearAuthentication whether the [SecurityContextLogoutHandler] should clear + * the [Authentication] at the time of logout. + * @property clearAuthentication whether to invalidate the [HttpSession] at the time of logout. + * @property logoutUrl the URL that triggers log out to occur. + * @property logoutRequestMatcher the [RequestMatcher] that triggers log out to occur. + * @property logoutSuccessUrl the URL to redirect to after logout has occurred. + * @property logoutSuccessHandler the [LogoutSuccessHandler] to use after logout has occurred. + * If this is specified, [logoutSuccessUrl] is ignored. + */ +class LogoutDsl { + var clearAuthentication: Boolean? = null + var invalidateHttpSession: Boolean? = null + var logoutUrl: String? = null + var logoutRequestMatcher: RequestMatcher? = null + var logoutSuccessUrl: String? = null + var logoutSuccessHandler: LogoutSuccessHandler? = null + var permitAll: Boolean? = null + + private var logoutHandlers = mutableListOf() + private var deleteCookies: Array? = null + private var defaultLogoutSuccessHandlerMappings: LinkedHashMap = linkedMapOf() + private var disabled = false + + + /** + * Adds a [LogoutHandler]. The [SecurityContextLogoutHandler] is added as + * the last [LogoutHandler] by default. + * + * @param logoutHandler the [LogoutHandler] to add + */ + fun addLogoutHandler(logoutHandler: LogoutHandler) { + this.logoutHandlers.add(logoutHandler) + } + + /** + * Allows specifying the names of cookies to be removed on logout success. + * + * @param cookieNamesToClear the names of cookies to be removed on logout success. + */ + fun deleteCookies(vararg cookieNamesToClear: String) { + this.deleteCookies = cookieNamesToClear + } + + /** + * Sets a default [LogoutSuccessHandler] to be used which prefers being + * invoked for the provided [RequestMatcher]. + * + * @param logoutHandler the [LogoutSuccessHandler] to use + * @param preferredMatcher the [RequestMatcher] for this default + * [AccessDeniedHandler] + */ + fun defaultLogoutSuccessHandlerFor(logoutHandler: LogoutSuccessHandler, preferredMatcher: RequestMatcher) { + defaultLogoutSuccessHandlerMappings[preferredMatcher] = logoutHandler + } + + /** + * Disables logout + */ + fun disable() { + disabled = true + } + + /** + * Grants access to the [logoutSuccessUrl] and the [logoutUrl] for every user. + */ + fun permitAll() { + permitAll = true + } + + internal fun get(): (LogoutConfigurer) -> Unit { + return { logout -> + clearAuthentication?.also { logout.clearAuthentication(clearAuthentication!!) } + invalidateHttpSession?.also { logout.invalidateHttpSession(invalidateHttpSession!!) } + logoutUrl?.also { logout.logoutUrl(logoutUrl) } + logoutRequestMatcher?.also { logout.logoutRequestMatcher(logoutRequestMatcher) } + logoutSuccessUrl?.also { logout.logoutSuccessUrl(logoutSuccessUrl) } + logoutSuccessHandler?.also { logout.logoutSuccessHandler(logoutSuccessHandler) } + deleteCookies?.also { logout.deleteCookies(*deleteCookies!!) } + permitAll?.also { logout.permitAll(permitAll!!) } + defaultLogoutSuccessHandlerMappings.forEach { (matcher, handler) -> + logout.defaultLogoutSuccessHandlerFor(handler, matcher) + } + logoutHandlers.forEach { logoutHandler -> + logout.addLogoutHandler(logoutHandler) + } + if (disabled) { + logout.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDsl.kt new file mode 100644 index 0000000000..a47234dadc --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDsl.kt @@ -0,0 +1,97 @@ +/* + * 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.servlet + +/* + * 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. + */ + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.oauth2.client.AuthorizationCodeGrantDsl +import org.springframework.security.config.web.servlet.oauth2.login.AuthorizationEndpointDsl +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository + +/** + * A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 client support using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizedClientService the service for authorized client(s). + */ +class OAuth2ClientDsl { + var clientRegistrationRepository: ClientRegistrationRepository? = null + var authorizedClientRepository: OAuth2AuthorizedClientRepository? = null + var authorizedClientService: OAuth2AuthorizedClientService? = null + + private var authorizationCodeGrant: ((OAuth2ClientConfigurer.AuthorizationCodeGrantConfigurer) -> Unit)? = null + + /** + * Configures the OAuth 2.0 Authorization Code Grant. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2Client { + * authorizationCodeGrant { + * authorizationRequestResolver = getAuthorizationRequestResolver() + * } + * } + * } + * } + * } + * ``` + * + * @param authorizationCodeGrantConfig custom configurations to configure the authorization + * code grant + * @see [AuthorizationEndpointDsl] + */ + fun authorizationCodeGrant(authorizationCodeGrantConfig: AuthorizationCodeGrantDsl.() -> Unit) { + this.authorizationCodeGrant = AuthorizationCodeGrantDsl().apply(authorizationCodeGrantConfig).get() + } + + internal fun get(): (OAuth2ClientConfigurer) -> Unit { + return { oauth2Client -> + clientRegistrationRepository?.also { oauth2Client.clientRegistrationRepository(clientRegistrationRepository) } + authorizedClientRepository?.also { oauth2Client.authorizedClientRepository(authorizedClientRepository) } + authorizedClientService?.also { oauth2Client.authorizedClientService(authorizedClientService) } + authorizationCodeGrant?.also { oauth2Client.authorizationCodeGrant(authorizationCodeGrant) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDsl.kt new file mode 100644 index 0000000000..3c505484cd --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDsl.kt @@ -0,0 +1,225 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.oauth2.login.AuthorizationEndpointDsl +import org.springframework.security.config.web.servlet.oauth2.login.RedirectionEndpointDsl +import org.springframework.security.config.web.servlet.oauth2.login.TokenEndpointDsl +import org.springframework.security.config.web.servlet.oauth2.login.UserInfoEndpointDsl +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.AuthenticationSuccessHandler + +/** + * A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizedClientService the service for authorized client(s). + * @property loginPage the login page to redirect to if authentication is required (i.e. + * "/login") + * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] used after + * authentication success + * @property authenticationFailureHandler the [AuthenticationFailureHandler] used after + * authentication success + * @property failureUrl the URL to send users if authentication fails + * @property loginProcessingUrl the URL to validate the credentials + * @property permitAll whether to grant access to the urls for [failureUrl] as well as + * for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user + */ +class OAuth2LoginDsl { + var clientRegistrationRepository: ClientRegistrationRepository? = null + var authorizedClientRepository: OAuth2AuthorizedClientRepository? = null + var authorizedClientService: OAuth2AuthorizedClientService? = null + var loginPage: String? = null + var authenticationSuccessHandler: AuthenticationSuccessHandler? = null + var authenticationFailureHandler: AuthenticationFailureHandler? = null + var failureUrl: String? = null + var loginProcessingUrl: String? = null + var permitAll: Boolean? = null + + private var defaultSuccessUrlOption: Pair? = null + private var authorizationEndpoint: ((OAuth2LoginConfigurer.AuthorizationEndpointConfig) -> Unit)? = null + private var tokenEndpoint: ((OAuth2LoginConfigurer.TokenEndpointConfig) -> Unit)? = null + private var redirectionEndpoint: ((OAuth2LoginConfigurer.RedirectionEndpointConfig) -> Unit)? = null + private var userInfoEndpoint: ((OAuth2LoginConfigurer.UserInfoEndpointConfig) -> Unit)? = null + + /** + * Grants access to the urls for [failureUrl] as well as for the [HttpSecurityBuilder], the + * [loginPage] and [loginProcessingUrl] for every user. + */ + fun permitAll() { + permitAll = true + } + + /** + * Specifies where users will be redirected after authenticating successfully if + * they have not visited a secured page prior to authenticating or [alwaysUse] + * is true. + * + * @param defaultSuccessUrl the default success url + * @param alwaysUse true if the [defaultSuccessUrl] should be used after + * authentication despite if a protected page had been previously visited + */ + fun defaultSuccessUrl(defaultSuccessUrl: String, alwaysUse: Boolean) { + defaultSuccessUrlOption = Pair(defaultSuccessUrl, alwaysUse) + } + + /** + * Configures the Authorization Server's Authorization Endpoint. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2Login { + * authorizationEndpoint { + * baseUri = "/auth" + * } + * } + * } + * } + * } + * ``` + * + * @param authorizationEndpointConfig custom configurations to configure the authorization + * endpoint + * @see [AuthorizationEndpointDsl] + */ + fun authorizationEndpoint(authorizationEndpointConfig: AuthorizationEndpointDsl.() -> Unit) { + this.authorizationEndpoint = AuthorizationEndpointDsl().apply(authorizationEndpointConfig).get() + } + + /** + * Configures the Authorization Server's Token Endpoint. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2Login { + * tokenEndpoint { + * accessTokenResponseClient = getAccessTokenResponseClient() + * } + * } + * } + * } + * } + * ``` + * + * @param tokenEndpointConfig custom configurations to configure the token + * endpoint + * @see [TokenEndpointDsl] + */ + fun tokenEndpoint(tokenEndpointConfig: TokenEndpointDsl.() -> Unit) { + this.tokenEndpoint = TokenEndpointDsl().apply(tokenEndpointConfig).get() + } + + /** + * Configures the Authorization Server's Redirection Endpoint. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2Login { + * redirectionEndpoint { + * baseUri = "/home" + * } + * } + * } + * } + * } + * ``` + * + * @param redirectionEndpointConfig custom configurations to configure the redirection + * endpoint + * @see [RedirectionEndpointDsl] + */ + fun redirectionEndpoint(redirectionEndpointConfig: RedirectionEndpointDsl.() -> Unit) { + this.redirectionEndpoint = RedirectionEndpointDsl().apply(redirectionEndpointConfig).get() + } + + /** + * Configures the Authorization Server's UserInfo Endpoint. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2Login { + * userInfoEndpoint { + * userService = getUserService() + * } + * } + * } + * } + * } + * ``` + * + * @param userInfoEndpointConfig custom configurations to configure the user info + * endpoint + * @see [UserInfoEndpointDsl] + */ + fun userInfoEndpoint(userInfoEndpointConfig: UserInfoEndpointDsl.() -> Unit) { + this.userInfoEndpoint = UserInfoEndpointDsl().apply(userInfoEndpointConfig).get() + } + + internal fun get(): (OAuth2LoginConfigurer) -> Unit { + return { oauth2Login -> + clientRegistrationRepository?.also { oauth2Login.clientRegistrationRepository(clientRegistrationRepository) } + authorizedClientRepository?.also { oauth2Login.authorizedClientRepository(authorizedClientRepository) } + authorizedClientService?.also { oauth2Login.authorizedClientService(authorizedClientService) } + loginPage?.also { oauth2Login.loginPage(loginPage) } + failureUrl?.also { oauth2Login.failureUrl(failureUrl) } + loginProcessingUrl?.also { oauth2Login.loginProcessingUrl(loginProcessingUrl) } + permitAll?.also { oauth2Login.permitAll(permitAll!!) } + defaultSuccessUrlOption?.also { + oauth2Login.defaultSuccessUrl(defaultSuccessUrlOption!!.first, defaultSuccessUrlOption!!.second) + } + authenticationSuccessHandler?.also { oauth2Login.successHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { oauth2Login.failureHandler(authenticationFailureHandler) } + authorizationEndpoint?.also { oauth2Login.authorizationEndpoint(authorizationEndpoint) } + tokenEndpoint?.also { oauth2Login.tokenEndpoint(tokenEndpoint) } + redirectionEndpoint?.also { oauth2Login.redirectionEndpoint(redirectionEndpoint) } + userInfoEndpoint?.also { oauth2Login.userInfoEndpoint(userInfoEndpoint) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDsl.kt new file mode 100644 index 0000000000..0487f1fee2 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDsl.kt @@ -0,0 +1,111 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.oauth2.resourceserver.JwtDsl +import org.springframework.security.config.web.servlet.oauth2.resourceserver.OpaqueTokenDsl +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.access.AccessDeniedHandler + +/** + * A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 resource server support using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property accessDeniedHandler the [AccessDeniedHandler] to use for requests authenticating + * with Bearer Tokens. + * @property authenticationEntryPoint the [AuthenticationEntryPoint] to use for requests authenticating + * with Bearer Tokens. + * @property bearerTokenResolver the [BearerTokenResolver] to use for requests authenticating + * with Bearer Tokens. + */ +class OAuth2ResourceServerDsl { + var accessDeniedHandler: AccessDeniedHandler? = null + var authenticationEntryPoint: AuthenticationEntryPoint? = null + var bearerTokenResolver: BearerTokenResolver? = null + + private var jwt: ((OAuth2ResourceServerConfigurer.JwtConfigurer) -> Unit)? = null + private var opaqueToken: ((OAuth2ResourceServerConfigurer.OpaqueTokenConfigurer) -> Unit)? = null + + /** + * Enables JWT-encoded bearer token support. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2ResourceServer { + * jwt { + * jwkSetUri = "https://example.com/oauth2/jwk" + * } + * } + * } + * } + * } + * ``` + * + * @param jwtConfig custom configurations to configure JWT resource server support + * @see [JwtDsl] + */ + fun jwt(jwtConfig: JwtDsl.() -> Unit) { + this.jwt = JwtDsl().apply(jwtConfig).get() + } + + /** + * Enables opaque token support. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * oauth2ResourceServer { + * opaqueToken { } + * } + * } + * } + * } + * ``` + * + * @param opaqueTokenConfig custom configurations to configure opaque token resource server support + * @see [OpaqueTokenDsl] + */ + fun opaqueToken(opaqueTokenConfig: OpaqueTokenDsl.() -> Unit) { + this.opaqueToken = OpaqueTokenDsl().apply(opaqueTokenConfig).get() + } + + internal fun get(): (OAuth2ResourceServerConfigurer) -> Unit { + return { oauth2ResourceServer -> + accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) } + authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) } + bearerTokenResolver?.also { oauth2ResourceServer.bearerTokenResolver(bearerTokenResolver) } + jwt?.also { oauth2ResourceServer.jwt(jwt) } + opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/PortMapperDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/PortMapperDsl.kt new file mode 100644 index 0000000000..4ad9954fe3 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/PortMapperDsl.kt @@ -0,0 +1,56 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.PortMapperConfigurer +import org.springframework.security.web.PortMapper + +/** + * A Kotlin DSL to configure a [PortMapper] for [HttpSecurity] using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property portMapper allows specifying the [PortMapper] instance. + */ +class PortMapperDsl { + private val mappings = mutableListOf>() + + var portMapper: PortMapper? = null + + /** + * Adds a mapping to the port mapper. + * + * @param fromPort the HTTP port number to map from + * @param toPort the HTTPS port number to map to + */ + fun map(fromPort: Int, toPort: Int) { + mappings.add(Pair(fromPort, toPort)) + } + + internal fun get(): (PortMapperConfigurer) -> Unit { + return { portMapperConfig -> + portMapper?.also { + portMapperConfig.portMapper(portMapper) + } + this.mappings.forEach { + portMapperConfig.http(it.first).mapsTo(it.second) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequestCacheDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequestCacheDsl.kt new file mode 100644 index 0000000000..b3cc6aa67c --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequestCacheDsl.kt @@ -0,0 +1,41 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer +import org.springframework.security.web.savedrequest.RequestCache + +/** + * A Kotlin DSL to enable request caching for [HttpSecurity] using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property requestCache allows explicit configuration of the [RequestCache] to be used + */ +class RequestCacheDsl { + var requestCache: RequestCache? = null + + internal fun get(): (RequestCacheConfigurer) -> Unit { + return { requestCacheConfig -> + requestCache?.also { + requestCacheConfig.requestCache(requestCache) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt new file mode 100644 index 0000000000..2febca5e67 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer +import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl +import org.springframework.security.web.access.channel.ChannelProcessor +import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.util.ClassUtils + +/** + * A Kotlin DSL to configure [HttpSecurity] channel security using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property channelProcessors the [ChannelProcessor] instances to use in + * [ChannelDecisionManagerImpl] + */ +class RequiresChannelDsl : AbstractRequestMatcherDsl() { + private val channelSecurityRules = mutableListOf() + + private val HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector" + private val MVC_PRESENT = ClassUtils.isPresent( + HANDLER_MAPPING_INTROSPECTOR, + RequiresChannelDsl::class.java.classLoader) + + var channelProcessors: List? = null + + /** + * Adds a channel security rule. + * + * @param matches the [RequestMatcher] to match incoming requests against + * @param attribute the configuration attribute to secure the matching request + * (i.e. "REQUIRES_SECURE_CHANNEL") + */ + fun secure(matches: RequestMatcher = AnyRequestMatcher.INSTANCE, + attribute: String = "REQUIRES_SECURE_CHANNEL") { + channelSecurityRules.add(MatcherAuthorizationRule(matches, attribute)) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param attribute the configuration attribute to secure the matching request + * (i.e. "REQUIRES_SECURE_CHANNEL") + */ + fun secure(pattern: String, attribute: String = "REQUIRES_SECURE_CHANNEL") { + if (MVC_PRESENT) { + channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, null, attribute)) + } else { + channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, null, attribute)) + } + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param pattern the pattern to match incoming requests against. + * @param servletPath the servlet path to match incoming requests against. This + * only applies when using an MVC pattern matcher. + * @param attribute the configuration attribute to secure the matching request + * (i.e. "REQUIRES_SECURE_CHANNEL") + */ + fun secure(pattern: String, servletPath: String, attribute: String = "REQUIRES_SECURE_CHANNEL") { + if (MVC_PRESENT) { + channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, servletPath, attribute)) + } else { + channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, servletPath, attribute)) + } + } + + /** + * Specify channel security is active. + */ + val requiresSecure = "REQUIRES_SECURE_CHANNEL" + + /** + * Specify channel security is inactive. + */ + val requiresInsecure = "REQUIRES_INSECURE_CHANNEL" + + internal fun get(): (ChannelSecurityConfigurer.ChannelRequestMatcherRegistry) -> Unit { + return { channelSecurity -> + channelProcessors?.also { channelSecurity.channelProcessors(channelProcessors) } + channelSecurityRules.forEach { rule -> + when (rule) { + is MatcherAuthorizationRule -> channelSecurity.requestMatchers(rule.matcher).requires(rule.rule) + is PatternAuthorizationRule -> { + when (rule.patternType) { + PatternType.ANT -> channelSecurity.antMatchers(rule.pattern).requires(rule.rule) + PatternType.MVC -> { + val mvcMatchersRequiresChannel = channelSecurity.mvcMatchers(rule.pattern) + rule.servletPath?.also { mvcMatchersRequiresChannel.servletPath(rule.servletPath) } + mvcMatchersRequiresChannel.requires(rule.rule) + } + } + } + } + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/Saml2Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/Saml2Dsl.kt new file mode 100644 index 0000000000..5475e1fab5 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/Saml2Dsl.kt @@ -0,0 +1,91 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.AuthenticationSuccessHandler + +/** + * A Kotlin DSL to configure [HttpSecurity] SAML2 login using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property relyingPartyRegistrationRepository the [RelyingPartyRegistrationRepository] of relying parties, + * each party representing a service provider, SP and this host, and identity provider, IDP pair that + * communicate with each other. + * @property loginPage the login page to redirect to if authentication is required (i.e. + * "/login") + * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] used after + * authentication success + * @property authenticationFailureHandler the [AuthenticationFailureHandler] used after + * authentication success + * @property failureUrl the URL to send users if authentication fails + * @property loginProcessingUrl the URL to validate the credentials + * @property permitAll whether to grant access to the urls for [failureUrl] as well as + * for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user + */ +class Saml2Dsl { + var relyingPartyRegistrationRepository: RelyingPartyRegistrationRepository? = null + var loginPage: String? = null + var authenticationSuccessHandler: AuthenticationSuccessHandler? = null + var authenticationFailureHandler: AuthenticationFailureHandler? = null + var failureUrl: String? = null + var loginProcessingUrl: String? = null + var permitAll: Boolean? = null + + private var defaultSuccessUrlOption: Pair? = null + + /** + * Grants access to the urls for [failureUrl] as well as for the [HttpSecurityBuilder], the + * [loginPage] and [loginProcessingUrl] for every user. + */ + fun permitAll() { + permitAll = true + } + + /** + * Specifies where users will be redirected after authenticating successfully if + * they have not visited a secured page prior to authenticating or [alwaysUse] + * is true. + * + * @param defaultSuccessUrl the default success url + * @param alwaysUse true if the [defaultSuccessUrl] should be used after + * authentication despite if a protected page had been previously visited + */ + fun defaultSuccessUrl(defaultSuccessUrl: String, alwaysUse: Boolean) { + defaultSuccessUrlOption = Pair(defaultSuccessUrl, alwaysUse) + } + + internal fun get(): (Saml2LoginConfigurer) -> Unit { + return { saml2Login -> + relyingPartyRegistrationRepository?.also { saml2Login.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) } + loginPage?.also { saml2Login.loginPage(loginPage) } + failureUrl?.also { saml2Login.failureUrl(failureUrl) } + loginProcessingUrl?.also { saml2Login.loginProcessingUrl(loginProcessingUrl) } + permitAll?.also { saml2Login.permitAll(permitAll!!) } + defaultSuccessUrlOption?.also { + saml2Login.defaultSuccessUrl(defaultSuccessUrlOption!!.first, defaultSuccessUrlOption!!.second) + } + authenticationSuccessHandler?.also { saml2Login.successHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { saml2Login.failureHandler(authenticationFailureHandler) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/SessionManagementDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/SessionManagementDsl.kt new file mode 100644 index 0000000000..d7fed24d0d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/SessionManagementDsl.kt @@ -0,0 +1,117 @@ +/* + * 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.servlet + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.session.SessionConcurrencyDsl +import org.springframework.security.config.web.servlet.session.SessionFixationDsl +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.authentication.AuthenticationFailureHandler +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy +import org.springframework.security.web.session.InvalidSessionStrategy + +/** + * A Kotlin DSL to configure [HttpSecurity] session management using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class SessionManagementDsl { + var invalidSessionUrl: String? = null + var invalidSessionStrategy: InvalidSessionStrategy? = null + var sessionAuthenticationErrorUrl: String? = null + var sessionAuthenticationFailureHandler: AuthenticationFailureHandler? = null + var enableSessionUrlRewriting: Boolean? = null + var sessionCreationPolicy: SessionCreationPolicy? = null + var sessionAuthenticationStrategy: SessionAuthenticationStrategy? = null + private var sessionFixation: ((SessionManagementConfigurer.SessionFixationConfigurer) -> Unit)? = null + private var sessionConcurrency: ((SessionManagementConfigurer.ConcurrencyControlConfigurer) -> Unit)? = null + + /** + * Enables session fixation protection. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * sessionManagement { + * sessionFixation { } + * } + * } + * } + * } + * ``` + * + * @param sessionFixationConfig custom configurations to configure session fixation + * protection + * @see [SessionFixationDsl] + */ + fun sessionFixation(sessionFixationConfig: SessionFixationDsl.() -> Unit) { + this.sessionFixation = SessionFixationDsl().apply(sessionFixationConfig).get() + } + + /** + * Controls the behaviour of multiple sessions for a user. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * httpSecurity(http) { + * sessionManagement { + * sessionConcurrency { + * maximumSessions = 1 + * maxSessionsPreventsLogin = true + * } + * } + * } + * } + * } + * ``` + * + * @param sessionConcurrencyConfig custom configurations to configure concurrency + * control + * @see [SessionConcurrencyDsl] + */ + fun sessionConcurrency(sessionConcurrencyConfig: SessionConcurrencyDsl.() -> Unit) { + this.sessionConcurrency = SessionConcurrencyDsl().apply(sessionConcurrencyConfig).get() + } + + internal fun get(): (SessionManagementConfigurer) -> Unit { + return { sessionManagement -> + invalidSessionUrl?.also { sessionManagement.invalidSessionUrl(invalidSessionUrl) } + invalidSessionStrategy?.also { sessionManagement.invalidSessionStrategy(invalidSessionStrategy) } + sessionAuthenticationErrorUrl?.also { sessionManagement.sessionAuthenticationErrorUrl(sessionAuthenticationErrorUrl) } + sessionAuthenticationFailureHandler?.also { sessionManagement.sessionAuthenticationFailureHandler(sessionAuthenticationFailureHandler) } + enableSessionUrlRewriting?.also { sessionManagement.enableSessionUrlRewriting(enableSessionUrlRewriting!!) } + sessionCreationPolicy?.also { sessionManagement.sessionCreationPolicy(sessionCreationPolicy) } + sessionAuthenticationStrategy?.also { sessionManagement.sessionAuthenticationStrategy(sessionAuthenticationStrategy) } + sessionFixation?.also { sessionManagement.sessionFixation(sessionFixation) } + sessionConcurrency?.also { sessionManagement.sessionConcurrency(sessionConcurrency) } + } + } +} + diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/X509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/X509Dsl.kt new file mode 100644 index 0000000000..bf41b4ec25 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/X509Dsl.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.servlet + +import org.springframework.security.authentication.AuthenticationDetailsSource +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.X509Configurer +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor +import javax.servlet.http.HttpServletRequest + +/** + * A Kotlin DSL to configure [HttpSecurity] X509 based pre authentication + * using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property x509AuthenticationFilter the entire [X509AuthenticationFilter]. If + * this is specified, the properties on [X509Configurer] will not be populated + * on the {@link X509AuthenticationFilter}. + * @property x509PrincipalExtractor the [X509PrincipalExtractor] + * @property authenticationDetailsSource the [X509PrincipalExtractor] + * @property userDetailsService shortcut for invoking + * [authenticationUserDetailsService] with a [UserDetailsByNameServiceWrapper] + * @property authenticationUserDetailsService the [AuthenticationUserDetailsService] to use + * @property subjectPrincipalRegex the regex to extract the principal from the certificate + */ +class X509Dsl { + var x509AuthenticationFilter: X509AuthenticationFilter? = null + var x509PrincipalExtractor: X509PrincipalExtractor? = null + var authenticationDetailsSource: AuthenticationDetailsSource? = null + var userDetailsService: UserDetailsService? = null + var authenticationUserDetailsService: AuthenticationUserDetailsService? = null + var subjectPrincipalRegex: String? = null + + internal fun get(): (X509Configurer) -> Unit { + return { x509 -> + x509AuthenticationFilter?.also { x509.x509AuthenticationFilter(x509AuthenticationFilter) } + x509PrincipalExtractor?.also { x509.x509PrincipalExtractor(x509PrincipalExtractor) } + authenticationDetailsSource?.also { x509.authenticationDetailsSource(authenticationDetailsSource) } + userDetailsService?.also { x509.userDetailsService(userDetailsService) } + authenticationUserDetailsService?.also { x509.authenticationUserDetailsService(authenticationUserDetailsService) } + subjectPrincipalRegex?.also { x509.subjectPrincipalRegex(subjectPrincipalRegex) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt new file mode 100644 index 0000000000..611d98b25a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt @@ -0,0 +1,46 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] cache control headers using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class CacheControlDsl { + private var disabled = false + + /** + * Disable cache control headers. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.CacheControlConfig) -> Unit { + return { cacheControl -> + if (disabled) { + cacheControl.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt new file mode 100644 index 0000000000..48c64a76b0 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt @@ -0,0 +1,47 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] Content-Security-Policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property policyDirectives the security policy directive(s) to be used in the response header. + * @property reportOnly includes the Content-Security-Policy-Report-Only header in the response. + */ +class ContentSecurityPolicyDsl { + var policyDirectives: String? = null + var reportOnly: Boolean? = null + + internal fun get(): (HeadersConfigurer.ContentSecurityPolicyConfig) -> Unit { + return { contentSecurityPolicy -> + policyDirectives?.also { + contentSecurityPolicy.policyDirectives(policyDirectives) + } + reportOnly?.also { + if (reportOnly!!) { + contentSecurityPolicy.reportOnly() + } + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt new file mode 100644 index 0000000000..1cdff0356e --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt @@ -0,0 +1,46 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure [HttpSecurity] X-Content-Type-Options header using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class ContentTypeOptionsDsl { + private var disabled = false + + /** + * Disable the X-Content-Type-Options header. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.ContentTypeOptionsConfig) -> Unit { + return { contentTypeOptions -> + if (disabled) { + contentTypeOptions.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt new file mode 100644 index 0000000000..dec22a52e3 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt @@ -0,0 +1,62 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] X-Frame-Options header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property sameOrigin allow any request that comes from the same origin to frame this + * application. + * @property deny deny framing any content from this application. + */ +class FrameOptionsDsl { + var sameOrigin: Boolean? = null + var deny: Boolean? = null + + private var disabled = false + + /** + * Disable the X-Frame-Options header. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.FrameOptionsConfig) -> Unit { + return { frameOptions -> + sameOrigin?.also { + if (sameOrigin!!) { + frameOptions.sameOrigin() + } + } + deny?.also { + if (deny!!) { + frameOptions.deny() + } + } + if (disabled) { + frameOptions.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt new file mode 100644 index 0000000000..d3d25532fa --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt @@ -0,0 +1,75 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] HTTP Public Key Pinning header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property pins the value for the pin- directive of the Public-Key-Pins header. + * @property maxAgeInSeconds the value (in seconds) for the max-age directive of the + * Public-Key-Pins header. + * @property includeSubDomains if true, the pinning policy applies to this pinned host + * as well as any subdomains of the host's domain name. + * @property reportOnly if true, the browser should not terminate the connection with + * the server. + * @property reportUri the URI to which the browser should report pin validation failures. + */ +class HttpPublicKeyPinningDsl { + var pins: Map? = null + var maxAgeInSeconds: Long? = null + var includeSubDomains: Boolean? = null + var reportOnly: Boolean? = null + var reportUri: String? = null + + private var disabled = false + + /** + * Disable the HTTP Public Key Pinning header. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.HpkpConfig) -> Unit { + return { hpkp -> + pins?.also { + hpkp.withPins(pins) + } + maxAgeInSeconds?.also { + hpkp.maxAgeInSeconds(maxAgeInSeconds!!) + } + includeSubDomains?.also { + hpkp.includeSubDomains(includeSubDomains!!) + } + reportOnly?.also { + hpkp.reportOnly(reportOnly!!) + } + reportUri?.also { + hpkp.reportUri(reportUri) + } + if (disabled) { + hpkp.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt new file mode 100644 index 0000000000..92c8da1530 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt @@ -0,0 +1,63 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.util.matcher.RequestMatcher + +/** + * A Kotlin DSL to configure the [HttpSecurity] HTTP Strict Transport Security header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property maxAgeInSeconds the value (in seconds) for the max-age directive of the + * Strict-Transport-Security header. + * @property requestMatcher the [RequestMatcher] used to determine if the + * "Strict-Transport-Security" header should be added. If true the header is added, + * else the header is not added. + * @property includeSubDomains if true, subdomains should be considered HSTS Hosts too. + * @property preload if true, preload will be included in HSTS Header. + */ +class HttpStrictTransportSecurityDsl { + var maxAgeInSeconds: Long? = null + var requestMatcher: RequestMatcher? = null + var includeSubDomains: Boolean? = null + var preload: Boolean? = null + + private var disabled = false + + /** + * Disable the HTTP Strict Transport Security header. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.HstsConfig) -> Unit { + return { hsts -> + maxAgeInSeconds?.also { hsts.maxAgeInSeconds(maxAgeInSeconds!!) } + requestMatcher?.also { hsts.requestMatcher(requestMatcher) } + 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/servlet/headers/ReferrerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt new file mode 100644 index 0000000000..226891e2d5 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt @@ -0,0 +1,41 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] referrer policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property policy the policy to be used in the response header. + */ +class ReferrerPolicyDsl { + var policy: ReferrerPolicyHeaderWriter.ReferrerPolicy? = null + + internal fun get(): (HeadersConfigurer.ReferrerPolicyConfig) -> Unit { + return { referrerPolicy -> + policy?.also { + referrerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt new file mode 100644 index 0000000000..3b919ef9c1 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt @@ -0,0 +1,55 @@ +/* + * 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.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] XSS protection header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property block whether to specify the mode as blocked + * @property xssProtectionEnabled if true, the header value will contain a value of 1. + * If false, will explicitly disable specify that X-XSS-Protection is disabled. + */ +class XssProtectionConfigDsl { + var block: Boolean? = null + var xssProtectionEnabled: Boolean? = null + + private var disabled = false + + /** + * Do not include the X-XSS-Protection header in the response. + */ + fun disable() { + disabled = true + } + + internal fun get(): (HeadersConfigurer.XXssConfig) -> Unit { + return { xssProtection -> + block?.also { xssProtection.block(block!!) } + xssProtectionEnabled?.also { xssProtection.xssProtectionEnabled(xssProtectionEnabled!!) } + + if (disabled) { + xssProtection.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt new file mode 100644 index 0000000000..ea0b6ae143 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.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.servlet.oauth2.client + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest + +/** + * A Kotlin DSL to configure OAuth 2.0 Authorization Code Grant. + * + * @author Eleftheria Stein + * @since 5.3 + * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]'s. + * @property authorizationRequestRepository the repository used for storing [OAuth2AuthorizationRequest]'s. + * @property accessTokenResponseClient the client used for requesting the access token credential + * from the Token Endpoint. + */ +class AuthorizationCodeGrantDsl { + var authorizationRequestResolver: OAuth2AuthorizationRequestResolver? = null + var authorizationRequestRepository: AuthorizationRequestRepository? = null + var accessTokenResponseClient: OAuth2AccessTokenResponseClient? = null + + internal fun get(): (OAuth2ClientConfigurer.AuthorizationCodeGrantConfigurer) -> Unit { + return { authorizationCodeGrant -> + authorizationRequestResolver?.also { authorizationCodeGrant.authorizationRequestResolver(authorizationRequestResolver) } + authorizationRequestRepository?.also { authorizationCodeGrant.authorizationRequestRepository(authorizationRequestRepository) } + accessTokenResponseClient?.also { authorizationCodeGrant.accessTokenResponseClient(accessTokenResponseClient) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt new file mode 100644 index 0000000000..b78928ee5f --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt @@ -0,0 +1,47 @@ +/* + * 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.servlet.oauth2.login + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest + +/** + * A Kotlin DSL to configure the Authorization Server's Authorization Endpoint using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property baseUri the base URI used for authorization requests. + * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]'s. + * @property authorizationRequestRepository the repository used for storing [OAuth2AuthorizationRequest]'s. + */ +class AuthorizationEndpointDsl { + var baseUri: String? = null + var authorizationRequestResolver: OAuth2AuthorizationRequestResolver? = null + var authorizationRequestRepository: AuthorizationRequestRepository? = null + + internal fun get(): (OAuth2LoginConfigurer.AuthorizationEndpointConfig) -> Unit { + return { authorizationEndpoint -> + baseUri?.also { authorizationEndpoint.baseUri(baseUri) } + authorizationRequestResolver?.also { authorizationEndpoint.authorizationRequestResolver(authorizationRequestResolver) } + authorizationRequestRepository?.also { authorizationEndpoint.authorizationRequestRepository(authorizationRequestRepository) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt new file mode 100644 index 0000000000..929e464260 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt @@ -0,0 +1,38 @@ +/* + * 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.servlet.oauth2.login + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer + +/** + * A Kotlin DSL to configure the Authorization Server's Redirection Endpoint using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property baseUri the URI where the authorization response will be processed. + */ +class RedirectionEndpointDsl { + var baseUri: String? = null + + internal fun get(): (OAuth2LoginConfigurer.RedirectionEndpointConfig) -> Unit { + return { redirectionEndpoint -> + baseUri?.also { redirectionEndpoint.baseUri(baseUri) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt new file mode 100644 index 0000000000..79aadfc15f --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt @@ -0,0 +1,41 @@ +/* + * 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.servlet.oauth2.login + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest + +/** + * A Kotlin DSL to configure the Authorization Server's Token Endpoint using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property accessTokenResponseClient the client used for requesting the access token credential + * from the Token Endpoint. + */ +class TokenEndpointDsl { + var accessTokenResponseClient: OAuth2AccessTokenResponseClient? = null + + internal fun get(): (OAuth2LoginConfigurer.TokenEndpointConfig) -> Unit { + return { tokenEndpoint -> + accessTokenResponseClient?.also { tokenEndpoint.accessTokenResponseClient(accessTokenResponseClient) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt new file mode 100644 index 0000000000..892753f1ce --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.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.servlet.oauth2.login + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest +import org.springframework.security.oauth2.client.registration.ClientRegistration +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.security.oauth2.core.user.OAuth2User + +/** + * A Kotlin DSL to configure the Authorization Server's UserInfo Endpoint using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property userService the OAuth 2.0 service used for obtaining the user attributes of the End-User + * from the UserInfo Endpoint. + * @property oidcUserService the OpenID Connect 1.0 service used for obtaining the user attributes of the + * End-User from the UserInfo Endpoint. + * @property userAuthoritiesMapper the [GrantedAuthoritiesMapper] used for mapping [OAuth2User.getAuthorities] + */ +class UserInfoEndpointDsl { + var userService: OAuth2UserService? = null + var oidcUserService: OAuth2UserService? = null + var userAuthoritiesMapper: GrantedAuthoritiesMapper? = null + + private var customUserTypePair: Pair, String>? = null + + /** + * Sets a custom [OAuth2User] type and associates it to the provided + * client [ClientRegistration.getRegistrationId] registration identifier. + * + * @param customUserType a custom [OAuth2User] type + * @param clientRegistrationId the client registration identifier + */ + fun customUserType(customUserType: Class, clientRegistrationId: String) { + customUserTypePair = Pair(customUserType, clientRegistrationId) + } + + internal fun get(): (OAuth2LoginConfigurer.UserInfoEndpointConfig) -> Unit { + return { userInfoEndpoint -> + userService?.also { userInfoEndpoint.userService(userService) } + oidcUserService?.also { userInfoEndpoint.oidcUserService(oidcUserService) } + userAuthoritiesMapper?.also { userInfoEndpoint.userAuthoritiesMapper(userAuthoritiesMapper) } + customUserTypePair?.also { userInfoEndpoint.customUserType(customUserTypePair!!.first, customUserTypePair!!.second) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt new file mode 100644 index 0000000000..db99bf8d43 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.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.servlet.oauth2.resourceserver + +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.JwtDecoder + +/** + * A Kotlin DSL to configure JWT Resource Server Support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property jwtAuthenticationConverter the [Converter] to use for converting a [Jwt] into + * an [AbstractAuthenticationToken]. + * @property jwtDecoder the [JwtDecoder] to use. + * @property jwkSetUri configures a [JwtDecoder] using a + * JSON Web Key (JWK) URL + */ +class JwtDsl { + var jwtAuthenticationConverter: Converter? = null + var jwtDecoder: JwtDecoder? = null + var jwkSetUri: String? = null + + internal fun get(): (OAuth2ResourceServerConfigurer.JwtConfigurer) -> Unit { + return { jwt -> + jwtAuthenticationConverter?.also { jwt.jwtAuthenticationConverter(jwtAuthenticationConverter) } + jwtDecoder?.also { jwt.decoder(jwtDecoder) } + jwkSetUri?.also { jwt.jwkSetUri(jwkSetUri) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt new file mode 100644 index 0000000000..42408134ce --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt @@ -0,0 +1,54 @@ +/* + * 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.servlet.oauth2.resourceserver + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector + +/** + * A Kotlin DSL to configure JWT Resource Server Support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property introspectionUri the URI of the Introspection endpoint. + * @property introspector the [OpaqueTokenIntrospector] to use. + */ +class OpaqueTokenDsl { + var introspectionUri: String? = null + var introspector: OpaqueTokenIntrospector? = null + + private var clientCredentials: Pair? = 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) + } + + internal fun get(): (OAuth2ResourceServerConfigurer.OpaqueTokenConfigurer) -> Unit { + return { opaqueToken -> + introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } + introspector?.also { opaqueToken.introspector(introspector) } + clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt new file mode 100644 index 0000000000..5be837c81f --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.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.servlet.session + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer +import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.web.session.SessionInformationExpiredStrategy + +/** + * A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + * @property maximumSessions controls the maximum number of sessions for a user. + * @property expiredUrl the URL to redirect to if a user tries to access a resource and + * their session has been expired due to too many sessions for the current user. + * @property expiredSessionStrategy determines the behaviour when an expired session + * is detected. + * @property maxSessionsPreventsLogin if true, prevents a user from authenticating when the + * [maximumSessions] has been reached. Otherwise (default), the user who authenticates + * is allowed access and an existing user's session is expired. + * @property sessionRegistry the [SessionRegistry] implementation used. + * + */ +class SessionConcurrencyDsl { + var maximumSessions: Int? = null + var expiredUrl: String? = null + var expiredSessionStrategy: SessionInformationExpiredStrategy? = null + var maxSessionsPreventsLogin: Boolean? = null + var sessionRegistry: SessionRegistry? = null + + internal fun get(): (SessionManagementConfigurer.ConcurrencyControlConfigurer) -> Unit { + return { sessionConcurrencyControl -> + maximumSessions?.also { + sessionConcurrencyControl.maximumSessions(maximumSessions!!) + } + expiredUrl?.also { + sessionConcurrencyControl.expiredUrl(expiredUrl) + } + expiredSessionStrategy?.also { + sessionConcurrencyControl.expiredSessionStrategy(expiredSessionStrategy) + } + maxSessionsPreventsLogin?.also { + sessionConcurrencyControl.maxSessionsPreventsLogin(maxSessionsPreventsLogin!!) + } + sessionRegistry?.also { + sessionConcurrencyControl.sessionRegistry(sessionRegistry) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt new file mode 100644 index 0000000000..7fa4c3d5cd --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.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.servlet.session + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpSession + +/** + * A Kotlin DSL to configure session fixation protection using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.3 + */ +class SessionFixationDsl { + private var strategy: SessionFixationStrategy? = null + + /** + * Specifies that a new session should be created, but the session attributes from + * the original [HttpSession] should not be retained. + */ + fun newSession() { + this.strategy = SessionFixationStrategy.NEW + } + + /** + * Specifies that a new session should be created and the session attributes from + * the original [HttpSession] should be retained. + */ + fun migrateSession() { + this.strategy = SessionFixationStrategy.MIGRATE + } + + /** + * Specifies that the Servlet container-provided session fixation protection + * should be used. When a session authenticates, the Servlet method + * [HttpServletRequest.changeSessionId] is called to change the session ID + * and retain all session attributes. + */ + fun changeSessionId() { + this.strategy = SessionFixationStrategy.CHANGE_ID + } + + /** + * Specifies that no session fixation protection should be enabled. + */ + fun none() { + this.strategy = SessionFixationStrategy.NONE + } + + internal fun get(): (SessionManagementConfigurer.SessionFixationConfigurer) -> Unit { + return { sessionFixation -> + strategy?.also { + when (strategy) { + SessionFixationStrategy.NEW -> sessionFixation.newSession() + SessionFixationStrategy.MIGRATE -> sessionFixation.migrateSession() + SessionFixationStrategy.CHANGE_ID -> sessionFixation.changeSessionId() + SessionFixationStrategy.NONE -> sessionFixation.none() + } + } + } + } +} + +private enum class SessionFixationStrategy { + NEW, MIGRATE, CHANGE_ID, NONE +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AnonymousDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AnonymousDslTests.kt new file mode 100644 index 0000000000..e89adfbce3 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AnonymousDslTests.kt @@ -0,0 +1,163 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import java.util.* + +/** + * Tests for [AnonymousDsl] + * + * @author Eleftheria Stein + */ +class AnonymousDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `anonymous when custom principal then custom principal used`() { + this.spring.register(PrincipalConfig::class.java, PrincipalController::class.java).autowire() + + this.mockMvc.get("/principal") + .andExpect { + content { string("principal") } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class PrincipalConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + anonymous { + principal = "principal" + } + } + } + } + + @Test + fun `anonymous when custom key then custom key used`() { + this.spring.register(KeyConfig::class.java, PrincipalController::class.java).autowire() + + this.mockMvc.get("/key") + .andExpect { + content { string("key".hashCode().toString()) } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class KeyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + anonymous { + key = "key" + } + } + } + } + + @Test + fun `anonymous when disabled then responds with forbidden`() { + this.spring.register(AnonymousDisabledConfig::class.java, PrincipalController::class.java).autowire() + + this.mockMvc.get("/principal") + .andExpect { + content { string("") } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AnonymousDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + anonymous { + disable() + } + } + } + } + + @Test + fun `anonymous when custom authorities then authorities used`() { + this.spring.register(AnonymousAuthoritiesConfig::class.java, PrincipalController::class.java).autowire() + + this.mockMvc.get("/principal") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AnonymousAuthoritiesConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + anonymous { + authorities = listOf(SimpleGrantedAuthority("TEST")) + } + authorizeRequests { + authorize(anyRequest, hasAuthority("TEST")) + } + } + } + } + + @RestController + internal class PrincipalController { + @GetMapping("/principal") + fun principal(@AuthenticationPrincipal principal: String?): String? { + return principal + } + + @GetMapping("/key") + fun key(): String { + return anonymousToken() + .map { it.keyHash } + .map { it.toString() } + .orElse(null) + } + + private fun anonymousToken(): Optional { + return Optional.of(SecurityContextHolder.getContext()) + .map { it.authentication } + .filter { it is AnonymousAuthenticationToken } + .map { AnonymousAuthenticationToken::class.java.cast(it) } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt new file mode 100644 index 0000000000..e1471d4a0c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt @@ -0,0 +1,211 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.util.matcher.RegexRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Tests for [AuthorizeRequestsDsl] + * + * @author Eleftheria Stein + */ +class AuthorizeRequestsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `request when secured by regex matcher then responds with forbidden`() { + this.spring.register(AuthorizeRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.get("/private") + .andExpect { + status { isForbidden } + } + } + + @Test + fun `request when allowed by regex matcher then responds with ok`() { + this.spring.register(AuthorizeRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class AuthorizeRequestsByRegexConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(RegexRequestMatcher("/path", null), permitAll) + authorize(RegexRequestMatcher(".*", null), authenticated) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when secured by mvc then responds with forbidden`() { + this.spring.register(AuthorizeRequestsByMvcConfig::class.java).autowire() + + this.mockMvc.get("/private") + .andExpect { + status { isForbidden } + } + } + + @Test + fun `request when allowed by mvc then responds with OK`() { + this.spring.register(AuthorizeRequestsByMvcConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk } + } + + this.mockMvc.get("/path.html") + .andExpect { + status { isOk } + } + + this.mockMvc.get("/path/") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AuthorizeRequestsByMvcConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/path", permitAll) + authorize("/**", authenticated) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when secured by mvc path variables then responds based on path variable value`() { + this.spring.register(MvcMatcherPathVariablesConfig::class.java).autowire() + + this.mockMvc.get("/user/user") + .andExpect { + status { isOk } + } + + this.mockMvc.get("/user/deny") + .andExpect { + status { isForbidden } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherPathVariablesConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/user/{userName}", "#userName == 'user'") + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/user/{user}") + fun path(@PathVariable user: String) { + } + } + } + + @Test + fun `request when secured by mvc with servlet path then responds based on servlet path`() { + this.spring.register(MvcMatcherServletPathConfig::class.java).autowire() + + this.mockMvc.perform(MockMvcRequestBuilders.get("/spring/path") + .with { request -> + request.servletPath = "/spring" + request + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(MockMvcRequestBuilders.get("/other/path") + .with { request -> + request.servletPath = "/other" + request + }) + .andExpect(status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherServletPathConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/path", + "/spring", + denyAll) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/CorsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CorsDslTests.kt new file mode 100644 index 0000000000..b9e23ec76d --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CorsDslTests.kt @@ -0,0 +1,138 @@ +/* + * 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.servlet + +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Tests for [CorsDsl] + * + * @author Eleftheria Stein + */ +class CorsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `CORS when no MVC then exception`() { + assertThatThrownBy { this.spring.register(DefaultCorsConfig::class.java).autowire() } + .isInstanceOf(BeanCreationException::class.java) + .hasMessageContaining("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext") + + } + + @EnableWebSecurity + open class DefaultCorsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + cors { } + } + } + } + + @Test + fun `CORS when CORS configuration source bean then responds with CORS header`() { + this.spring.register(CorsCrossOriginConfig::class.java).autowire() + + this.mockMvc.get("/") + { + header(HttpHeaders.ORIGIN, "https://example.com") + }.andExpect { + header { exists("Access-Control-Allow-Origin") } + } + } + + @EnableWebMvc + @EnableWebSecurity + open class CorsCrossOriginConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + cors { } + } + } + + @Bean + open fun corsConfigurationSource(): CorsConfigurationSource { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + corsConfiguration.allowedMethods = listOf( + RequestMethod.GET.name, + RequestMethod.POST.name) + source.registerCorsConfiguration("/**", corsConfiguration) + return source + } + } + + @Test + fun `CORS when disabled then response does not include CORS header`() { + this.spring.register(CorsDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") + { + header(HttpHeaders.ORIGIN, "https://example.com") + }.andExpect { + header { doesNotExist("Access-Control-Allow-Origin") } + } + } + + @EnableWebMvc + @EnableWebSecurity + open class CorsDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http.cors() + http { + cors { + disable() + } + } + } + + @Bean + open fun corsConfigurationSource(): CorsConfigurationSource { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + corsConfiguration.allowedMethods = listOf( + RequestMethod.GET.name, + RequestMethod.POST.name) + source.registerCorsConfiguration("/**", corsConfiguration) + return source + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt new file mode 100644 index 0000000000..d62ff5c8b9 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt @@ -0,0 +1,265 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.Authentication +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy +import org.springframework.security.web.csrf.CsrfTokenRepository +import org.springframework.security.web.csrf.DefaultCsrfToken +import org.springframework.security.web.util.matcher.AntPathRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RestController +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Tests for [CsrfDsl] + * + * @author Eleftheria Stein + */ +class CsrfDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `POST when CSRF enabled and no CSRF token then forbidden`() { + this.spring.register(DefaultCsrfConfig::class.java).autowire() + + this.mockMvc.post("/test1") + .andExpect { + status { isForbidden } + } + } + + @Test + fun `POST when CSRF enabled and CSRF token then status OK`() { + this.spring.register(DefaultCsrfConfig::class.java, BasicController::class.java).autowire() + + this.mockMvc.post("/test1") { + with(csrf()) + }.andExpect { + status { isOk } + } + + } + + @EnableWebSecurity + open class DefaultCsrfConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + csrf { } + } + } + } + + @Test + fun `POST when CSRF disabled and no CSRF token then status OK`() { + this.spring.register(CsrfDisabledConfig::class.java, BasicController::class.java).autowire() + + this.mockMvc.post("/test1") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class CsrfDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + csrf { + disable() + } + } + } + } + + @Test + fun `CSRF when custom CSRF token repository then repo used`() { + `when`(CustomRepositoryConfig.REPO.loadToken(any())) + .thenReturn(DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")) + + this.spring.register(CustomRepositoryConfig::class.java).autowire() + + this.mockMvc.get("/test1") + + verify(CustomRepositoryConfig.REPO).loadToken(any()) + } + + @EnableWebSecurity + open class CustomRepositoryConfig : WebSecurityConfigurerAdapter() { + companion object { + var REPO: CsrfTokenRepository = mock(CsrfTokenRepository::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + csrf { + csrfTokenRepository = REPO + } + } + } + } + + @Test + fun `CSRF when require CSRF protection matcher then CSRF protection on matching requests`() { + this.spring.register(RequireCsrfProtectionMatcherConfig::class.java, BasicController::class.java).autowire() + + this.mockMvc.post("/test1") + .andExpect { + status { isForbidden } + } + + this.mockMvc.post("/test2") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class RequireCsrfProtectionMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + csrf { + requireCsrfProtectionMatcher = AntPathRequestMatcher("/test1") + } + } + } + } + + @Test + fun `CSRF when custom session authentication strategy then strategy used`() { + this.spring.register(CustomStrategyConfig::class.java).autowire() + + this.mockMvc.perform(formLogin()) + + verify(CustomStrategyConfig.STRATEGY, atLeastOnce()) + .onAuthentication(any(Authentication::class.java), any(HttpServletRequest::class.java), any(HttpServletResponse::class.java)) + + } + + @EnableWebSecurity + open class CustomStrategyConfig : WebSecurityConfigurerAdapter() { + companion object { + var STRATEGY: SessionAuthenticationStrategy = mock(SessionAuthenticationStrategy::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + formLogin { } + csrf { + sessionAuthenticationStrategy = STRATEGY + } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + + @Test + fun `CSRF when ignoring request matchers then CSRF disabled on matching requests`() { + this.spring.register(IgnoringRequestMatchersConfig::class.java, BasicController::class.java).autowire() + + this.mockMvc.post("/test1") + .andExpect { + status { isForbidden } + } + + this.mockMvc.post("/test2") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class IgnoringRequestMatchersConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + csrf { + requireCsrfProtectionMatcher = AntPathRequestMatcher("/**") + ignoringRequestMatchers(AntPathRequestMatcher("/test2")) + } + } + } + } + + @Test + fun `CSRF when ignoring ant matchers then CSRF disabled on matching requests`() { + this.spring.register(IgnoringAntMatchersConfig::class.java, BasicController::class.java).autowire() + + this.mockMvc.post("/test1") + .andExpect { + status { isForbidden } + } + + this.mockMvc.post("/test2") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class IgnoringAntMatchersConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + csrf { + requireCsrfProtectionMatcher = AntPathRequestMatcher("/**") + ignoringAntMatchers("/test2") + } + } + } + } + + @RestController + internal class BasicController { + @PostMapping("/test1") + fun test1() { + } + + @PostMapping("/test2") + fun test2() { + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDslTests.kt new file mode 100644 index 0000000000..dda4904b9e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/ExceptionHandlingDslTests.kt @@ -0,0 +1,252 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.userdetails.User.withUsername +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.security.web.access.AccessDeniedHandlerImpl +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.util.matcher.AntPathRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Tests for [ExceptionHandlingDsl] + * + * @author Eleftheria Stein + */ +class ExceptionHandlingDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `request when exception handling enabled then returns forbidden`() { + this.spring.register(ExceptionHandlingConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isForbidden } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class ExceptionHandlingConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + exceptionHandling { } + } + } + } + + @Test(expected = AccessDeniedException::class) + fun `request when exception handling disabled then throws exception`() { + this.spring.register(ExceptionHandlingDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") + } + + @EnableWebSecurity + open class ExceptionHandlingDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + exceptionHandling { + disable() + } + } + } + } + + @Test + fun `exception handling when custom access denied page then redirects to custom page`() { + this.spring.register(AccessDeniedPageConfig::class.java).autowire() + + this.mockMvc.get("/admin") { + with(user(withUsername("user").password("password").roles("USER").build())) + }.andExpect { + status { isForbidden } + forwardedUrl("/access-denied") + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AccessDeniedPageConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/admin", hasAuthority("ROLE_ADMIN")) + authorize(anyRequest, authenticated) + } + exceptionHandling { + accessDeniedPage = "/access-denied" + } + } + } + } + + @Test + fun `exception handling when custom access denied handler then handler used`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + + this.mockMvc.get("/admin") { + with(user(withUsername("user").password("password").roles("USER").build())) + }.andExpect { + status { isForbidden } + forwardedUrl("/access-denied") + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AccessDeniedHandlerConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val customAccessDeniedHandler = AccessDeniedHandlerImpl() + customAccessDeniedHandler.setErrorPage("/access-denied") + http { + authorizeRequests { + authorize("/admin", hasAuthority("ROLE_ADMIN")) + authorize(anyRequest, authenticated) + } + exceptionHandling { + accessDeniedHandler = customAccessDeniedHandler + } + } + } + } + + @Test + fun `exception handling when default access denied handler for page then handlers used`() { + this.spring.register(AccessDeniedHandlerForConfig::class.java).autowire() + + this.mockMvc.get("/admin1") { + with(user(withUsername("user").password("password").roles("USER").build())) + }.andExpect { + status { isForbidden } + forwardedUrl("/access-denied1") + } + + this.mockMvc.get("/admin2") { + with(user(withUsername("user").password("password").roles("USER").build())) + }.andExpect { + status { isForbidden } + forwardedUrl("/access-denied2") + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AccessDeniedHandlerForConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val customAccessDeniedHandler1 = AccessDeniedHandlerImpl() + customAccessDeniedHandler1.setErrorPage("/access-denied1") + val customAccessDeniedHandler2 = AccessDeniedHandlerImpl() + customAccessDeniedHandler2.setErrorPage("/access-denied2") + http { + authorizeRequests { + authorize("/admin1", hasAuthority("ROLE_ADMIN")) + authorize("/admin2", hasAuthority("ROLE_ADMIN")) + authorize(anyRequest, authenticated) + } + exceptionHandling { + defaultAccessDeniedHandlerFor(customAccessDeniedHandler1, AntPathRequestMatcher("/admin1")) + defaultAccessDeniedHandlerFor(customAccessDeniedHandler2, AntPathRequestMatcher("/admin2")) + } + } + } + } + + @Test + fun `exception handling when custom authentication entry point then entry point used`() { + this.spring.register(AuthenticationEntryPointConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isFound } + redirectedUrl("http://localhost/custom-login") + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AuthenticationEntryPointConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + exceptionHandling { + authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/custom-login") + } + } + } + } + + @Test + fun `exception handling when authentication entry point for page then entry points used`() { + this.spring.register(AuthenticationEntryPointForConfig::class.java).autowire() + + this.mockMvc.get("/secured1") + .andExpect { + status { isFound } + redirectedUrl("http://localhost/custom-login1") + } + + this.mockMvc.get("/secured2") + .andExpect { + status { isFound } + redirectedUrl("http://localhost/custom-login2") + } + } + + @EnableWebSecurity + @EnableWebMvc + open class AuthenticationEntryPointForConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val customAuthenticationEntryPoint1 = LoginUrlAuthenticationEntryPoint("/custom-login1") + val customAuthenticationEntryPoint2 = LoginUrlAuthenticationEntryPoint("/custom-login2") + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + exceptionHandling { + defaultAuthenticationEntryPointFor(customAuthenticationEntryPoint1, AntPathRequestMatcher("/secured1")) + defaultAuthenticationEntryPointFor(customAuthenticationEntryPoint2, AntPathRequestMatcher("/secured2")) + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/FormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/FormLoginDslTests.kt new file mode 100644 index 0000000000..e1f7c5b0fb --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/FormLoginDslTests.kt @@ -0,0 +1,291 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.userdetails.User +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler +import org.springframework.stereotype.Controller +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.GetMapping + +/** + * Tests for [FormLoginDsl] + * + * @author Eleftheria Stein + */ +class FormLoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `login page when form login configured then default login page created`() { + this.spring.register(FormLoginConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.get("/login") + .andExpect { + status { isOk } + } + } + + @Test + fun `login when success then redirects to home`() { + this.spring.register(FormLoginConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin()) + .andExpect { + status().isFound + redirectedUrl("/") + } + } + + @Test + fun `login when failure then redirects to login page with error`() { + this.spring.register(FormLoginConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin().password("invalid")) + .andExpect { + status().isFound + redirectedUrl("/login?error") + } + } + + @EnableWebSecurity + open class FormLoginConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin {} + } + } + } + + @Test + fun `request when secure then redirects to default login page`() { + this.spring.register(AllSecuredConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isFound } + redirectedUrl("http://localhost/login") + } + } + + @EnableWebSecurity + open class AllSecuredConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin {} + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `request when secure and custom login page then redirects to custom login page`() { + this.spring.register(LoginPageConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isFound } + redirectedUrl("http://localhost/log-in") + } + } + + @EnableWebSecurity + open class LoginPageConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + loginPage = "/log-in" + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `login when custom success handler then used`() { + this.spring.register(SuccessHandlerConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin()) + .andExpect { + status().isFound + redirectedUrl("/success") + } + } + + @EnableWebSecurity + open class SuccessHandlerConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + authenticationSuccessHandler = SimpleUrlAuthenticationSuccessHandler("/success") + } + } + } + } + + @Test + fun `login when custom failure handler then used`() { + this.spring.register(FailureHandlerConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin().password("invalid")) + .andExpect { + status().isFound + redirectedUrl("/failure") + } + } + + @EnableWebSecurity + open class FailureHandlerConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + authenticationFailureHandler = SimpleUrlAuthenticationFailureHandler("/failure") + } + } + } + } + + @Test + fun `login when custom failure url then used`() { + this.spring.register(FailureHandlerConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin().password("invalid")) + .andExpect { + status().isFound + redirectedUrl("/failure") + } + } + + @EnableWebSecurity + open class FailureUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + failureUrl = "/failure" + } + } + } + } + + @Test + fun `login when custom login processing url then used`() { + this.spring.register(LoginProcessingUrlConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin("/custom")) + .andExpect { + status().isFound + redirectedUrl("/") + } + } + + @EnableWebSecurity + open class LoginProcessingUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + loginProcessingUrl = "/custom" + } + } + } + } + + @Test + fun `login when default success url then redirected to url`() { + this.spring.register(DefaultSuccessUrlConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.perform(formLogin()) + .andExpect { + status().isFound + redirectedUrl("/custom") + } + } + + @EnableWebSecurity + open class DefaultSuccessUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + formLogin { + defaultSuccessUrl("/custom", true) + } + } + } + } + + @Test + fun `login when permit all then login page not protected`() { + this.spring.register(PermitAllConfig::class.java, UserConfig::class.java).autowire() + + this.mockMvc.get("/custom/login") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class PermitAllConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + formLogin { + loginPage = "/custom/login" + permitAll() + } + } + } + + @Controller + class LoginController { + @GetMapping("/custom/login") + fun loginPage() {} + } + } + + @Configuration + open class UserConfig { + @Autowired + fun configureGlobal(auth: AuthenticationManagerBuilder) { + auth + .inMemoryAuthentication() + .withUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt new file mode 100644 index 0000000000..7824a7fafe --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt @@ -0,0 +1,94 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +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.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [HeadersDsl] + * + * @author Eleftheria Stein + */ +class HeadersDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when defaults enabled then default headers in response`() { + this.spring.register(DefaultHeadersConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") } + header { string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) } + header { string(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") } + header { string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") } + header { string(HttpHeaders.EXPIRES, "0") } + header { string(HttpHeaders.PRAGMA, "no-cache") } + header { string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block") } + } + } + + @EnableWebSecurity + open class DefaultHeadersConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { } + } + } + } + + @Test + fun `headers when feature policy configured then header in response`() { + this.spring.register(FeaturePolicyConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("Feature-Policy", "geolocation 'self'") } + } + } + + @EnableWebSecurity + open class FeaturePolicyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + featurePolicy(policyDirectives = "geolocation 'self'") + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpBasicDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpBasicDslTests.kt new file mode 100644 index 0000000000..3a45d81075 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpBasicDslTests.kt @@ -0,0 +1,205 @@ +/* + * 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.servlet + +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.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationDetailsSource +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.AuthenticationException +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Tests for [HttpBasicDsl] + * + * @author Eleftheria Stein + */ +class HttpBasicDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `http basic when configured then insecure request cannot access`() { + this.spring.register(HttpBasicConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isUnauthorized } + } + } + + @Test + fun `http basic when configured then response includes basic challenge`() { + this.spring.register(HttpBasicConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("WWW-Authenticate", "Basic realm=\"Realm\"") } + } + } + + @Test + fun `http basic when valid user then permitted`() { + this.spring.register(HttpBasicConfig::class.java, UserConfig::class.java, MainController::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("user", "password")) + }.andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class HttpBasicConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + httpBasic {} + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun httpBasicWhenCustomRealmThenUsed() { + this.spring.register(CustomRealmConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("WWW-Authenticate", "Basic realm=\"Custom Realm\"") } + } + } + + @EnableWebSecurity + open class CustomRealmConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + httpBasic { + realmName = "Custom Realm" + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `http basic when custom authentication entry point then used`() { + this.spring.register(CustomAuthenticationEntryPointConfig::class.java).autowire() + + this.mockMvc.get("/") + + verify(CustomAuthenticationEntryPointConfig.ENTRY_POINT) + .commence(any(HttpServletRequest::class.java), + any(HttpServletResponse::class.java), + any(AuthenticationException::class.java)) + } + + @EnableWebSecurity + open class CustomAuthenticationEntryPointConfig : WebSecurityConfigurerAdapter() { + companion object { + var ENTRY_POINT: AuthenticationEntryPoint = mock(AuthenticationEntryPoint::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + httpBasic { + authenticationEntryPoint = ENTRY_POINT + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `http basic when custom authentication details source then used`() { + this.spring.register(CustomAuthenticationDetailsSourceConfig::class.java, + UserConfig::class.java, MainController::class.java).autowire() + + this.mockMvc.get("/") { + with(httpBasic("username", "password")) + } + + verify(CustomAuthenticationDetailsSourceConfig.AUTHENTICATION_DETAILS_SOURCE) + .buildDetails(any(HttpServletRequest::class.java)) + } + + @EnableWebSecurity + open class CustomAuthenticationDetailsSourceConfig : WebSecurityConfigurerAdapter() { + companion object { + var AUTHENTICATION_DETAILS_SOURCE = mock(AuthenticationDetailsSource::class.java) as AuthenticationDetailsSource + } + + override fun configure(http: HttpSecurity) { + http { + httpBasic { + authenticationDetailsSource = AUTHENTICATION_DETAILS_SOURCE + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Configuration + open class UserConfig { + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + + @RestController + class MainController { + @GetMapping("/") + fun main() { + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt new file mode 100644 index 0000000000..4c8fc8d12c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt @@ -0,0 +1,215 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +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.util.matcher.RegexRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Tests for [HttpSecurityDsl] + * + * @author Eleftheria Stein + */ +class HttpSecurityDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `post when default security configured then CSRF prevents the request`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.mockMvc.post("/") + .andExpect { + status { isForbidden } + } + } + + @Test + fun `when default security configured then default headers are in the response`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { + string(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + } + header { + string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + } + header { + string(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + } + header { + string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + } + header { + string(HttpHeaders.EXPIRES, "0") + } + header { + string(HttpHeaders.PRAGMA, "no-cache") + } + header { + string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block") + } + } + } + + @EnableWebSecurity + open class DefaultSecurityConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http {} + } + + @Configuration + open class UserConfig { + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + } + + @Test + fun `request when it does not match the security request matcher then the security rules do not apply`() { + this.spring.register(SecurityRequestMatcherConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isNotFound } + } + } + + @Test + fun `request when it matches the security request matcher then the security rules apply`() { + this.spring.register(SecurityRequestMatcherConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isForbidden } + } + } + + @EnableWebSecurity + open class SecurityRequestMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + securityMatcher(RegexRequestMatcher("/path", null)) + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `request when it does not match the security pattern matcher then the security rules do not apply`() { + this.spring.register(SecurityPatternMatcherConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isNotFound } + } + } + + @Test + fun `request when it matches the security pattern matcher then the security rules apply`() { + this.spring.register(SecurityPatternMatcherConfig::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isForbidden } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class SecurityPatternMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + securityMatcher("/path") + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `security pattern matcher when used with security request matcher then both apply`() { + this.spring.register(MultiMatcherConfig::class.java).autowire() + + this.mockMvc.get("/path1") + .andExpect { + status { isForbidden } + } + + this.mockMvc.get("/path2") + .andExpect { + status { isForbidden } + } + + this.mockMvc.get("/path3") + .andExpect { + status { isNotFound } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class MultiMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + securityMatcher("/path1") + securityMatcher(RegexRequestMatcher("/path2", null)) + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/LogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/LogoutDslTests.kt new file mode 100644 index 0000000000..350a288bd5 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/LogoutDslTests.kt @@ -0,0 +1,310 @@ +/* + * 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.servlet + +import org.assertj.core.api.Assertions.assertThat +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.mock.web.MockHttpSession +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf +import org.springframework.security.web.authentication.logout.LogoutHandler +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.security.web.util.matcher.AntPathRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post + +/** + * Tests for [LogoutDsl] + * + * @author Eleftheria Stein + */ +class LogoutDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `logout when custom logout url then custom url used`() { + this.spring.register(CustomLogoutUrlConfig::class.java).autowire() + + this.mockMvc.post("/custom/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + } + + @EnableWebSecurity + open class CustomLogoutUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + logoutUrl = "/custom/logout" + } + } + } + } + + @Test + fun `logout when custom logout request matcher then custom request matcher used`() { + this.spring.register(CustomLogoutRequestMatcherConfig::class.java).autowire() + + this.mockMvc.post("/custom/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + } + + @EnableWebSecurity + open class CustomLogoutRequestMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + logoutRequestMatcher = AntPathRequestMatcher("/custom/logout") + } + } + } + } + + @Test + fun `logout when custom success url then redirects to success url`() { + this.spring.register(SuccessUrlConfig::class.java).autowire() + + this.mockMvc.post("/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login") + } + } + + @EnableWebSecurity + open class SuccessUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + logoutSuccessUrl = "/login" + } + } + } + } + + @Test + fun `logout when custom success handler then redirects to success url`() { + this.spring.register(SuccessHandlerConfig::class.java).autowire() + + this.mockMvc.post("/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/") + } + } + + @EnableWebSecurity + open class SuccessHandlerConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + logoutSuccessHandler = SimpleUrlLogoutSuccessHandler() + } + } + } + } + + @Test + fun `logout when permit all then logout allowed`() { + this.spring.register(PermitAllConfig::class.java).autowire() + + this.mockMvc.post("/custom/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + } + + @EnableWebSecurity + open class PermitAllConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + logout { + logoutUrl = "/custom/logout" + permitAll() + } + } + } + } + + @Test + fun `logout when clear authentication false then authentication not cleared`() { + this.spring.register(ClearAuthenticationFalseConfig::class.java).autowire() + val currentContext = SecurityContextHolder.createEmptyContext() + currentContext.authentication = TestingAuthenticationToken("user", "password", "ROLE_USER") + val currentSession = MockHttpSession() + currentSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, currentContext) + + this.mockMvc.post("/logout") { + with(csrf()) + session = currentSession + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + + assertThat(currentContext.authentication).isNotNull + } + + @EnableWebSecurity + open class ClearAuthenticationFalseConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + clearAuthentication = false + } + } + } + } + + @Test + fun `logout when invalidate http session false then session not invalidated`() { + this.spring.register(InvalidateHttpSessionFalseConfig::class.java).autowire() + val currentSession = MockHttpSession() + + this.mockMvc.post("/logout") { + with(csrf()) + session = currentSession + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + + assertThat(currentSession.isInvalid).isFalse() + } + + @EnableWebSecurity + open class InvalidateHttpSessionFalseConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + invalidateHttpSession = false + } + } + } + } + + @Test + fun `logout when delete cookies then cookies are cleared`() { + this.spring.register(DeleteCookiesConfig::class.java).autowire() + + this.mockMvc.post("/logout") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + cookie { maxAge("remove", 0) } + } + } + + @EnableWebSecurity + open class DeleteCookiesConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + deleteCookies("remove") + } + } + } + } + + @Test + fun `logout when default logout success handler for request then custom handler used`() { + this.spring.register(DefaultLogoutSuccessHandlerForConfig::class.java).autowire() + + this.mockMvc.post("/logout/default") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/login?logout") + } + + this.mockMvc.post("/logout/custom") { + with(csrf()) + }.andExpect { + status { isFound } + redirectedUrl("/") + } + } + + @EnableWebSecurity + open class DefaultLogoutSuccessHandlerForConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + logout { + logoutRequestMatcher = AntPathRequestMatcher("/logout/**") + defaultLogoutSuccessHandlerFor(SimpleUrlLogoutSuccessHandler(), AntPathRequestMatcher("/logout/custom")) + } + } + } + } + + @Test + fun `logout when custom logout handler then custom handler used`() { + this.spring.register(CustomLogoutHandlerConfig::class.java).autowire() + + this.mockMvc.post("/logout") { + with(csrf()) + } + + verify(CustomLogoutHandlerConfig.HANDLER).logout(any(), any(), any()) + } + + @EnableWebSecurity + open class CustomLogoutHandlerConfig : WebSecurityConfigurerAdapter() { + companion object { + var HANDLER: LogoutHandler = mock(LogoutHandler::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + logout { + addLogoutHandler(HANDLER) + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDslTests.kt new file mode 100644 index 0000000000..ce74d8ef0e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ClientDslTests.kt @@ -0,0 +1,144 @@ +/* + * 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.servlet + +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.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository +import org.springframework.security.oauth2.core.OAuth2AccessToken +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [OAuth2ClientDsl] + * + * @author Eleftheria Stein + */ +class OAuth2ClientDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Client when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebSecurity + open class ClientRepoConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + clientRegistrationRepository = InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `oauth2Client when custom authorized client repository then repository used`() { + this.spring.register(ClientRepositoryConfig::class.java, ClientConfig::class.java).autowire() + val authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("test") + .clientId("clientId") + .authorizationUri("https://test") + .redirectUri("http://localhost/callback") + .attributes(mapOf(Pair(OAuth2ParameterNames.REGISTRATION_ID, "registrationId"))) + .build() + `when`(ClientRepositoryConfig.REQUEST_REPOSITORY.loadAuthorizationRequest(any())) + .thenReturn(authorizationRequest) + `when`(ClientRepositoryConfig.REQUEST_REPOSITORY.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest) + `when`(ClientRepositoryConfig.CLIENT.getTokenResponse(any())) + .thenReturn(OAuth2AccessTokenResponse + .withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build()) + + this.mockMvc.get("/callback") { + param("state", "test") + param("code", "123") + } + + verify(ClientRepositoryConfig.CLIENT_REPOSITORY).saveAuthorizedClient(any(), any(), any(), any()) + } + + @EnableWebSecurity + open class ClientRepositoryConfig : WebSecurityConfigurerAdapter() { + companion object { + var REQUEST_REPOSITORY: AuthorizationRequestRepository = mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + var CLIENT: OAuth2AccessTokenResponseClient = mock(OAuth2AccessTokenResponseClient::class.java) as OAuth2AccessTokenResponseClient + var CLIENT_REPOSITORY: OAuth2AuthorizedClientRepository = mock(OAuth2AuthorizedClientRepository::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + authorizedClientRepository = CLIENT_REPOSITORY + authorizationCodeGrant { + authorizationRequestRepository = REQUEST_REPOSITORY + accessTokenResponseClient = CLIENT + } + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .registrationId("registrationId") + .clientId("clientId") + .clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDslTests.kt new file mode 100644 index 0000000000..6eaff435a7 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2LoginDslTests.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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +/** + * Tests for [OAuth2LoginDsl] + * + * @author Eleftheria Stein + */ +class OAuth2LoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Login when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebSecurity + open class ClientRepoConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2Login { + clientRegistrationRepository = InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `login page when oAuth2Login configured then default login page created`() { + this.spring.register(OAuth2LoginConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/login") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class OAuth2LoginConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2Login { } + } + } + } + + @Test + fun `login page when custom login page then redirected to custom page`() { + this.spring.register(LoginPageConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/custom-login") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class LoginPageConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2Login { + loginPage = "/custom-login" + } + } + } + + @RestController + class LoginController { + @GetMapping("/custom-login") + fun loginPage() { } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDslTests.kt new file mode 100644 index 0000000000..2ac4326244 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/OAuth2ResourceServerDslTests.kt @@ -0,0 +1,156 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [OAuth2ResourceServerDsl] + * + * @author Eleftheria Stein + */ +class OAuth2ResourceServerDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Resource server when custom entry point then entry point used`() { + this.spring.register(EntryPointConfig::class.java).autowire() + + this.mockMvc.get("/") + + verify(EntryPointConfig.ENTRY_POINT).commence(any(), any(), any()) + } + + @EnableWebSecurity + open class EntryPointConfig : WebSecurityConfigurerAdapter() { + companion object { + var ENTRY_POINT: AuthenticationEntryPoint = mock(AuthenticationEntryPoint::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + authenticationEntryPoint = ENTRY_POINT + jwt { } + } + } + } + + @Bean + open fun jwtDecoder(): JwtDecoder { + return mock(JwtDecoder::class.java) + } + } + + @Test + fun `oauth2Resource server when custom bearer token resolver then resolver used`() { + this.spring.register(BearerTokenResolverConfig::class.java).autowire() + + this.mockMvc.get("/") + + verify(BearerTokenResolverConfig.RESOLVER).resolve(any()) + } + + @EnableWebSecurity + open class BearerTokenResolverConfig : WebSecurityConfigurerAdapter() { + companion object { + var RESOLVER: BearerTokenResolver = mock(BearerTokenResolver::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + bearerTokenResolver = RESOLVER + jwt { } + } + } + } + + @Bean + open fun jwtDecoder(): JwtDecoder { + return mock(JwtDecoder::class.java) + } + } + + @Test + fun `oauth2Resource server when custom access denied handler then handler used`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + `when`(AccessDeniedHandlerConfig.DECODER.decode(anyString())).thenReturn( + Jwt.withTokenValue("token") + .header("alg", "none") + .claim(SUB, "user") + .build()) + + this.mockMvc.get("/") { + header("Authorization", "Bearer token") + } + + verify(AccessDeniedHandlerConfig.DENIED_HANDLER).handle(any(), any(), any()) + } + + @EnableWebSecurity + open class AccessDeniedHandlerConfig : WebSecurityConfigurerAdapter() { + companion object { + var DENIED_HANDLER: AccessDeniedHandler = mock(AccessDeniedHandler::class.java) + var DECODER: JwtDecoder = mock(JwtDecoder::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, denyAll) + } + oauth2ResourceServer { + accessDeniedHandler = DENIED_HANDLER + jwt { } + } + } + } + + @Bean + open fun jwtDecoder(): JwtDecoder { + return DECODER + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/PortMapperDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/PortMapperDslTests.kt new file mode 100644 index 0000000000..54af6cff7e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/PortMapperDslTests.kt @@ -0,0 +1,93 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.PortMapperImpl +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.* + +/** + * Tests for [PortMapperDsl] + * + * @author Eleftheria Stein + */ +class PortMapperDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `port mapper when specifying map then redirects to https port`() { + this.spring.register(PortMapperMapConfig::class.java).autowire() + + this.mockMvc.get("http://localhost:543") + .andExpect { + redirectedUrl("https://localhost:123") + } + } + + @EnableWebSecurity + open class PortMapperMapConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + requiresChannel { + secure(anyRequest, requiresSecure) + } + portMapper { + map(543, 123) + } + } + } + } + + @Test + fun `port mapper when specifying custom mapper then redirects to https port`() { + this.spring.register(CustomPortMapperConfig::class.java).autowire() + + this.mockMvc.get("http://localhost:543") + .andExpect { + redirectedUrl("https://localhost:123") + } + } + + @EnableWebSecurity + open class CustomPortMapperConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val customPortMapper = PortMapperImpl() + customPortMapper.setPortMappings(Collections.singletonMap("543", "123")) + http { + requiresChannel { + secure(anyRequest, requiresSecure) + } + portMapper { + portMapper = customPortMapper + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequestCacheDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequestCacheDslTests.kt new file mode 100644 index 0000000000..cd59b084c1 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequestCacheDslTests.kt @@ -0,0 +1,90 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin +import org.springframework.security.web.savedrequest.NullRequestCache +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl + +/** + * Tests for [RequestCacheDsl] + * + * @author Eleftheria Stein + */ +class RequestCacheDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `GET when request cache enabled then redirected to cached page`() { + this.spring.register(RequestCacheConfig::class.java).autowire() + + this.mockMvc.get("/test") + + this.mockMvc.perform(formLogin()) + .andExpect { + redirectedUrl("http://localhost/test") + } + } + + @EnableWebSecurity + open class RequestCacheConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + requestCache { } + formLogin { } + } + } + } + + @Test + fun `GET when custom request cache then custom request cache used`() { + this.spring.register(CustomRequestCacheConfig::class.java).autowire() + + this.mockMvc.get("/test") + + this.mockMvc.perform(formLogin()) + .andExpect { + redirectedUrl("/") + } + } + + @EnableWebSecurity + open class CustomRequestCacheConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + requestCache { + requestCache = NullRequestCache() + } + formLogin { } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDslTests.kt new file mode 100644 index 0000000000..21bc9b692d --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDslTests.kt @@ -0,0 +1,138 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.access.channel.ChannelProcessor +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Tests for [RequiresChannelDsl] + * + * @author Eleftheria Stein + */ +class RequiresChannelDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `requires channel when requires secure then redirects to https`() { + this.spring.register(RequiresSecureConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + redirectedUrl("https://localhost/") + } + } + + @EnableWebSecurity + open class RequiresSecureConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + requiresChannel { + secure(anyRequest, requiresSecure) + } + } + } + } + + @Test + fun `request when channel matches mvc with servlet path then redirects based on servlet path`() { + this.spring.register(MvcMatcherServletPathConfig::class.java).autowire() + + this.mockMvc.perform(MockMvcRequestBuilders.get("/spring/path") + .with { request -> + request.servletPath = "/spring" + request + }) + .andExpect(status().isFound) + .andExpect(redirectedUrl("https://localhost/spring/path")) + + this.mockMvc.perform(MockMvcRequestBuilders.get("/other/path") + .with { request -> + request.servletPath = "/other" + request + }) + .andExpect(MockMvcResultMatchers.status().isOk) + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherServletPathConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + requiresChannel { + secure("/path", + "/spring", + requiresSecure) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `requires channel when channel processors configured then channel processors used`() { + `when`(ChannelProcessorsConfig.CHANNEL_PROCESSOR.supports(any())).thenReturn(true) + this.spring.register(ChannelProcessorsConfig::class.java).autowire() + + this.mockMvc.get("/") + + verify(ChannelProcessorsConfig.CHANNEL_PROCESSOR).supports(any()) + } + + @EnableWebSecurity + open class ChannelProcessorsConfig : WebSecurityConfigurerAdapter() { + companion object { + var CHANNEL_PROCESSOR: ChannelProcessor = mock(ChannelProcessor::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + requiresChannel { + channelProcessors = listOf(CHANNEL_PROCESSOR) + secure(anyRequest, requiresSecure) + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt new file mode 100644 index 0000000000..519aa14b4e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/Saml2DslTests.kt @@ -0,0 +1,105 @@ +/* + * 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.servlet + +import org.assertj.core.api.Assertions +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.io.ClassPathResource +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.saml2.credentials.Saml2X509Credential +import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.security.cert.Certificate +import java.security.cert.CertificateFactory + +/** + * Tests for [Saml2Dsl] + * + * @author Eleftheria Stein + */ +class Saml2DslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `saml2Login when no relying party registration repository then exception`() { + Assertions.assertThatThrownBy { this.spring.register(Saml2LoginNoRelyingPArtyRegistrationRepoConfig::class.java).autowire() } + .isInstanceOf(BeanCreationException::class.java) + .hasMessageContaining("relyingPartyRegistrationRepository cannot be null") + + } + + @EnableWebSecurity + open class Saml2LoginNoRelyingPArtyRegistrationRepoConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + saml2Login { } + } + } + } + + @Test + fun `login page when saml2Configured then default login page created`() { + this.spring.register(Saml2LoginConfig::class.java).autowire() + + this.mockMvc.get("/login") + .andExpect { + status { isOk } + } + } + + @EnableWebSecurity + open class Saml2LoginConfig : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + saml2Login { + relyingPartyRegistrationRepository = + InMemoryRelyingPartyRegistrationRepository( + RelyingPartyRegistration.withRegistrationId("samlId") + .remoteIdpEntityId("entityId") + .assertionConsumerServiceUrlTemplate("{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI) + .credentials { c -> c.add(Saml2X509Credential(loadCert("rod.cer"), VERIFICATION)) } + .idpWebSsoUrl("ssoUrl") + .build() + ) + } + } + } + + 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/servlet/SessionManagementDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/SessionManagementDslTests.kt new file mode 100644 index 0000000000..460281028b --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/SessionManagementDslTests.kt @@ -0,0 +1,218 @@ +/* + * 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.servlet + +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.annotation.Bean +import org.springframework.mock.web.MockHttpSession +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.Authentication +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler +import org.springframework.security.web.authentication.session.SessionAuthenticationException +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy +import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Tests for [SessionManagementDsl] + * + * @author Eleftheria Stein + */ +class SessionManagementDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `session management when invalid session url then redirected to url`() { + this.spring.register(InvalidSessionUrlConfig::class.java).autowire() + + this.mockMvc.perform(get("/") + .with { request -> + request.isRequestedSessionIdValid = false + request.requestedSessionId = "id" + request + }) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/invalid")) + } + + @EnableWebSecurity + open class InvalidSessionUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + invalidSessionUrl = "/invalid" + } + } + } + } + + @Test + fun `session management when invalid session strategy then strategy used`() { + this.spring.register(InvalidSessionStrategyConfig::class.java).autowire() + + this.mockMvc.perform(get("/") + .with { request -> + request.isRequestedSessionIdValid = false + request.requestedSessionId = "id" + request + }) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/invalid")) + } + + @EnableWebSecurity + open class InvalidSessionStrategyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + invalidSessionStrategy = SimpleRedirectInvalidSessionStrategy("/invalid") + } + } + } + } + + @Test + fun `session management when session authentication error url then redirected to url`() { + this.spring.register(SessionAuthenticationErrorUrlConfig::class.java).autowire() + val session = mock(MockHttpSession::class.java) + `when`(session.changeSessionId()).thenThrow(SessionAuthenticationException::class.java) + + this.mockMvc.perform(get("/") + .with(authentication(mock(Authentication::class.java))) + .session(session)) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/session-auth-error")) + } + + @EnableWebSecurity + open class SessionAuthenticationErrorUrlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + sessionManagement { + sessionAuthenticationErrorUrl = "/session-auth-error" + } + } + } + } + + @Test + fun `session management when session authentication failure handler then handler used`() { + this.spring.register(SessionAuthenticationFailureHandlerConfig::class.java).autowire() + val session = mock(MockHttpSession::class.java) + `when`(session.changeSessionId()).thenThrow(SessionAuthenticationException::class.java) + + this.mockMvc.perform(get("/") + .with(authentication(mock(Authentication::class.java))) + .session(session)) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/session-auth-error")) + } + + @EnableWebSecurity + open class SessionAuthenticationFailureHandlerConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + sessionManagement { + sessionAuthenticationFailureHandler = SimpleUrlAuthenticationFailureHandler("/session-auth-error") + } + } + } + } + + @Test + fun `session management when stateless policy then does not store session`() { + this.spring.register(StatelessSessionManagementConfig::class.java).autowire() + + val result = this.mockMvc.perform(get("/")) + .andReturn() + + assertThat(result.request.getSession(false)).isNull() + } + + @EnableWebSecurity + open class StatelessSessionManagementConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + sessionManagement { + sessionCreationPolicy = SessionCreationPolicy.STATELESS + } + } + } + } + + @Test + fun `session management when session authentication strategy then strategy used`() { + this.spring.register(SessionAuthenticationStrategyConfig::class.java).autowire() + + this.mockMvc.perform(get("/") + .with(authentication(mock(Authentication::class.java))) + .session(mock(MockHttpSession::class.java))) + + verify(this.spring.getContext().getBean(SessionAuthenticationStrategy::class.java)) + .onAuthentication(any(Authentication::class.java), + any(HttpServletRequest::class.java), any(HttpServletResponse::class.java)) + } + + @EnableWebSecurity + open class SessionAuthenticationStrategyConfig : WebSecurityConfigurerAdapter() { + var mockSessionAuthenticationStrategy: SessionAuthenticationStrategy = mock(SessionAuthenticationStrategy::class.java) + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + sessionManagement { + sessionAuthenticationStrategy = mockSessionAuthenticationStrategy + } + } + } + + @Bean + open fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy { + return this.mockSessionAuthenticationStrategy + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/X509DslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/X509DslTests.kt new file mode 100644 index 0000000000..0b5d0483c6 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/X509DslTests.kt @@ -0,0 +1,221 @@ +/* + * 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.servlet + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.core.io.ClassPathResource +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509 +import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +/** + * Tests for [X509Dsl] + * + * @author Eleftheria Stein + */ +class X509DslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `x509 when configured with defaults then user authenticated`() { + this.spring.register(X509Config::class.java).autowire() + val certificate = loadCert("rod.cer") + + this.mockMvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")) + } + + @EnableWebSecurity + open class X509Config : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + x509 { } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + + @Test + fun `x509 when configured with regex then user authenticated`() { + this.spring.register(X509RegexConfig::class.java).autowire() + val certificate = loadCert("rodatexampledotcom.cer") + + this.mockMvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")) + } + + @EnableWebSecurity + open class X509RegexConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + x509 { + subjectPrincipalRegex = "CN=(.*?)@example.com(?:,|$)" + } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + + @Test + fun `x509 when user details service configured then user details service used`() { + this.spring.register(UserDetailsServiceConfig::class.java).autowire() + val certificate = loadCert("rod.cer") + + this.mockMvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")) + } + + @EnableWebSecurity + open class UserDetailsServiceConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val userDetails = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + val customUserDetailsService = InMemoryUserDetailsManager(userDetails) + http { + x509 { + userDetailsService = customUserDetailsService + } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + return mock(UserDetailsService::class.java) + } + } + + @Test + fun `x509 when authentication user details service configured then custom user details service used`() { + this.spring.register(AuthenticationUserDetailsServiceConfig::class.java).autowire() + val certificate = loadCert("rod.cer") + + this.mockMvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")) + } + + @EnableWebSecurity + open class AuthenticationUserDetailsServiceConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val userDetails = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + val customUserDetailsService = InMemoryUserDetailsManager(userDetails) + val customSource = UserDetailsByNameServiceWrapper() + customSource.setUserDetailsService(customUserDetailsService) + http { + x509 { + authenticationUserDetailsService = customSource + } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + return mock(UserDetailsService::class.java) + } + } + + @Test + fun `x509 when configured with principal extractor then principal extractor used`() { + this.spring.register(X509PrincipalExtractorConfig::class.java).autowire() + val certificate = loadCert("rodatexampledotcom.cer") + + this.mockMvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")) + } + + @EnableWebSecurity + open class X509PrincipalExtractorConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + val principalExtractor = SubjectDnX509PrincipalExtractor() + principalExtractor.setSubjectDnRegex("CN=(.*?)@example.com(?:,|$)") + http { + x509 { + x509PrincipalExtractor = principalExtractor + } + } + } + + @Bean + override fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } + + 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/servlet/headers/CacheControlDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDslTests.kt new file mode 100644 index 0000000000..8773d76aae --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDslTests.kt @@ -0,0 +1,92 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [CacheControlDsl] + * + * @author Eleftheria Stein + */ +class CacheControlDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when cache control configured then cache control headers in response`() { + this.spring.register(CacheControlConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") } + header { string(HttpHeaders.EXPIRES, "0") } + header { string(HttpHeaders.PRAGMA, "no-cache") } + } + } + + @EnableWebSecurity + open class CacheControlConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + cacheControl { } + } + } + } + } + + @Test + fun `headers when cache control disabled then no cache control headers in response`() { + this.spring.register(CacheControlDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { doesNotExist(HttpHeaders.CACHE_CONTROL) } + header { doesNotExist(HttpHeaders.EXPIRES) } + header { doesNotExist(HttpHeaders.PRAGMA) } + } + } + + @EnableWebSecurity + open class CacheControlDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + cacheControl { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDslTests.kt new file mode 100644 index 0000000000..09807431b4 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDslTests.kt @@ -0,0 +1,116 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [ContentSecurityPolicyDsl] + * + * @author Eleftheria Stein + */ +class ContentSecurityPolicyDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when content security policy configured then header in response`() { + this.spring.register(ContentSecurityPolicyConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'") } + } + } + + @EnableWebSecurity + open class ContentSecurityPolicyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + contentSecurityPolicy { } + } + } + } + } + + @Test + fun `headers when content security policy configured with custom policy directives then custom directives in header`() { + this.spring.register(CustomPolicyDirectivesConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'; script-src trustedscripts.example.com") } + } + } + + @EnableWebSecurity + open class CustomPolicyDirectivesConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + contentSecurityPolicy { + policyDirectives = "default-src 'self'; script-src trustedscripts.example.com" + } + } + } + } + } + + @Test + fun `headers when report only content security policy report only header in response`() { + this.spring.register(ReportOnlyConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY, "default-src 'self'") } + } + } + + @EnableWebSecurity + open class ReportOnlyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + contentSecurityPolicy { + reportOnly = true + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDslTests.kt new file mode 100644 index 0000000000..44d2bc32a4 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDslTests.kt @@ -0,0 +1,88 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [ContentTypeOptionsDsl] + * + * @author Eleftheria Stein + */ +class ContentTypeOptionsDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when content type options configured then X-Content-Type-Options header in response`() { + this.spring.register(ContentTypeOptionsConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") } + } + } + + @EnableWebSecurity + open class ContentTypeOptionsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + contentTypeOptions { } + } + } + } + } + + @Test + fun `headers when content type options disabled then X-Content-Type-Options header not in response`() { + this.spring.register(ContentTypeOptionsDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) } + } + } + + @EnableWebSecurity + open class ContentTypeOptionsDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + contentTypeOptions { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDslTests.kt new file mode 100644 index 0000000000..4aae10c47e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDslTests.kt @@ -0,0 +1,167 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [FrameOptionsDsl] + * + * @author Eleftheria Stein + */ +class FrameOptionsDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when frame options configured then frame options deny header`() { + this.spring.register(FrameOptionsConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) } + } + } + + @EnableWebSecurity + open class FrameOptionsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + frameOptions { } + } + } + } + } + + @Test + fun `headers when frame options deny configured then frame options deny header`() { + this.spring.register(FrameOptionsDenyConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) } + } + } + + @EnableWebSecurity + open class FrameOptionsDenyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + frameOptions { + deny = true + } + } + } + } + } + + @Test + fun `headers when frame options same origin configured then frame options same origin header`() { + this.spring.register(FrameOptionsSameOriginConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN.name) } + } + } + + @EnableWebSecurity + open class FrameOptionsSameOriginConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + frameOptions { + sameOrigin = true + } + } + } + } + } + + @Test + fun `headers when frame options same origin and deny configured then frame options deny header`() { + this.spring.register(FrameOptionsSameOriginAndDenyConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) } + } + } + + @EnableWebSecurity + open class FrameOptionsSameOriginAndDenyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + frameOptions { + sameOrigin = true + deny = true + } + } + } + } + } + + @Test + fun `headers when frame options disabled then no frame options header in response`() { + this.spring.register(FrameOptionsDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) } + } + } + + @EnableWebSecurity + open class FrameOptionsDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + frameOptions { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDslTests.kt new file mode 100644 index 0000000000..7facec036d --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDslTests.kt @@ -0,0 +1,230 @@ +/* + * 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.servlet.headers + +import org.assertj.core.api.Assertions +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.servlet.invoke +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [HttpPublicKeyPinningDsl] + * + * @author Eleftheria Stein + */ +class HttpPublicKeyPinningDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + private val HPKP_RO_HEADER_NAME = "Public-Key-Pins-Report-Only" + private val HPKP_HEADER_NAME = "Public-Key-Pins" + + @Test + fun `headers when HPKP configured and no pin then no headers in response`() { + this.spring.register(HpkpNoPinConfig::class.java).autowire() + + val result = this.mockMvc.get("/") { + secure = true + }.andReturn() + + Assertions.assertThat(result.response.headerNames).isEmpty() + } + + @EnableWebSecurity + open class HpkpNoPinConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { } + } + } + } + } + + @Test + fun `headers when HPKP configured with pin then header in response`() { + this.spring.register(HpkpPinConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(HPKP_RO_HEADER_NAME, "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"") } + } + } + + @EnableWebSecurity + open class HpkpPinConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { + pins = mapOf(Pair("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256")) + } + } + } + } + } + + @Test + fun `headers when HPKP configured with maximum age then maximum age in header`() { + this.spring.register(HpkpMaxAgeConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(HPKP_RO_HEADER_NAME, "max-age=604800 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"") } + } + } + + @EnableWebSecurity + open class HpkpMaxAgeConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { + pins = mapOf(Pair("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256")) + maxAgeInSeconds = 604800 + } + } + } + } + } + + @Test + fun `headers when HPKP configured with report only false then public key pins header in response`() { + this.spring.register(HpkpReportOnlyFalseConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(HPKP_HEADER_NAME, "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"") } + } + } + + @EnableWebSecurity + open class HpkpReportOnlyFalseConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { + pins = mapOf(Pair("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256")) + reportOnly = false + } + } + } + } + } + + @Test + fun `headers when HPKP configured with include subdomains then include subdomains in header`() { + this.spring.register(HpkpIncludeSubdomainsConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { + string(HPKP_RO_HEADER_NAME, + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; includeSubDomains") + } + } + } + + @EnableWebSecurity + open class HpkpIncludeSubdomainsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { + pins = mapOf(Pair("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256")) + includeSubDomains = true + } + } + } + } + } + + @Test + fun `headers when HPKP configured with report uri then report uri in header`() { + this.spring.register(HpkpReportUriConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { + string(HPKP_RO_HEADER_NAME, + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; report-uri=\"https://example.com\"") + } + } + } + + @EnableWebSecurity + open class HpkpReportUriConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpPublicKeyPinning { + pins = mapOf(Pair("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256")) + reportUri = "https://example.com" + } + } + } + } + } + + @Test + fun `headers when HPKP disabled then no HPKP header in response`() { + this.spring.register(HpkpDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { + doesNotExist(HPKP_RO_HEADER_NAME) + } + } + } + + @EnableWebSecurity + open class HpkpDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + httpPublicKeyPinning { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDslTests.kt new file mode 100644 index 0000000000..adb13bcce0 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDslTests.kt @@ -0,0 +1,167 @@ +/* + * 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.servlet.headers + +import org.assertj.core.api.Assertions +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.util.matcher.AntPathRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [HttpStrictTransportSecurityDsl] + * + * @author Eleftheria Stein + */ +class HttpStrictTransportSecurityDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when hsts configured then headers in response`() { + this.spring.register(HstsConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") } + } + } + + @EnableWebSecurity + open class HstsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpStrictTransportSecurity { } + } + } + } + } + + @Test + fun `headers when hsts configured with preload then preload in header`() { + this.spring.register(HstsPreloadConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains ; preload") } + } + } + + @EnableWebSecurity + open class HstsPreloadConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpStrictTransportSecurity { + preload = true + } + } + } + } + } + + @Test + fun `headers when hsts configured with max age then max age in header`() { + this.spring.register(HstsMaxAgeConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=1 ; includeSubDomains") } + } + } + + @EnableWebSecurity + open class HstsMaxAgeConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpStrictTransportSecurity { + maxAgeInSeconds = 1 + } + } + } + } + } + + @Test + fun `headers when hsts configured and does not match then hsts header not in response`() { + this.spring.register(HstsCustomMatcherConfig::class.java).autowire() + + val result = this.mockMvc.get("/") { + secure = true + }.andReturn() + + Assertions.assertThat(result.response.headerNames).isEmpty() + } + + @EnableWebSecurity + open class HstsCustomMatcherConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + httpStrictTransportSecurity { + requestMatcher = AntPathRequestMatcher("/secure/**") + } + } + } + } + } + + @Test + fun `request when hsts disabled then hsts header not in response`() { + this.spring.register(HstsDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) } + } + } + + @EnableWebSecurity + open class HstsDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + httpStrictTransportSecurity { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDslTests.kt new file mode 100644 index 0000000000..2551e1f920 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDslTests.kt @@ -0,0 +1,89 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [ReferrerPolicyDsl] + * + * @author Eleftheria Stein + */ +class ReferrerPolicyDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when referrer policy configured then header in response`() { + this.spring.register(ReferrerPolicyConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER.policy) } + } + } + + @EnableWebSecurity + open class ReferrerPolicyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + referrerPolicy { } + } + } + } + } + + @Test + fun `headers when referrer policy configured with custom policy then custom policy in header`() { + this.spring.register(ReferrerPolicyCustomPolicyConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN.policy) } + } + } + + @EnableWebSecurity + open class ReferrerPolicyCustomPolicyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + referrerPolicy { + policy = ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt new file mode 100644 index 0000000000..d1ab6b7361 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt @@ -0,0 +1,140 @@ +/* + * 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.servlet.headers + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [XssProtectionConfigDsl] + * + * @author Eleftheria Stein + */ +class XssProtectionConfigDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `headers when XSS protection configured then header in response`() { + this.spring.register(XssProtectionConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block") } + } + } + + @EnableWebSecurity + open class XssProtectionConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + xssProtection { } + } + } + } + } + + @Test + fun `headers when XSS protection with block false then mode is not block in header`() { + this.spring.register(XssProtectionBlockFalseConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1") } + } + } + + @EnableWebSecurity + open class XssProtectionBlockFalseConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + xssProtection { + block = false + } + } + } + } + } + + @Test + fun `headers when XSS protection disabled then X-XSS-Protection header is 0`() { + this.spring.register(XssProtectionDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "0") } + } + } + + @EnableWebSecurity + open class XssProtectionDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + defaultsDisabled = true + xssProtection { + xssProtectionEnabled = false + } + } + } + } + } + + @Test + fun `headers when XSS protection disabled then X-XSS-Protection header not in response`() { + this.spring.register(XssProtectionDisabledFunctionConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) } + } + } + + @EnableWebSecurity + open class XssProtectionDisabledFunctionConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + xssProtection { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDslTests.kt new file mode 100644 index 0000000000..2573d90dfb --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDslTests.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.servlet.oauth2.client + +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.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.core.OAuth2AccessToken +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [AuthorizationCodeGrantDsl] + * + * @author Eleftheria Stein + */ +class AuthorizationCodeGrantDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Client when custom authorization request repository then repository used`() { + this.spring.register(RequestRepositoryConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/callback") { + param("state", "test") + param("code", "123") + } + + verify(RequestRepositoryConfig.REQUEST_REPOSITORY).loadAuthorizationRequest(any()) + } + + @EnableWebSecurity + open class RequestRepositoryConfig : WebSecurityConfigurerAdapter() { + companion object { + var REQUEST_REPOSITORY: AuthorizationRequestRepository = Mockito.mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + authorizationCodeGrant { + authorizationRequestRepository = REQUEST_REPOSITORY + } + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `oauth2Client when custom access token response client then client used`() { + this.spring.register(AuthorizedClientConfig::class.java, ClientConfig::class.java).autowire() + val authorizationRequest = getOAuth2AuthorizationRequest() + Mockito.`when`(AuthorizedClientConfig.REQUEST_REPOSITORY.loadAuthorizationRequest(any())) + .thenReturn(authorizationRequest) + Mockito.`when`(AuthorizedClientConfig.REQUEST_REPOSITORY.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest) + Mockito.`when`(AuthorizedClientConfig.CLIENT.getTokenResponse(any())) + .thenReturn(OAuth2AccessTokenResponse + .withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build()) + + this.mockMvc.get("/callback") { + param("state", "test") + param("code", "123") + } + + verify(AuthorizedClientConfig.CLIENT).getTokenResponse(any()) + } + + @EnableWebSecurity + open class AuthorizedClientConfig : WebSecurityConfigurerAdapter() { + companion object { + var REQUEST_REPOSITORY: AuthorizationRequestRepository = Mockito.mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + var CLIENT: OAuth2AccessTokenResponseClient = Mockito.mock(OAuth2AccessTokenResponseClient::class.java) as OAuth2AccessTokenResponseClient + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + authorizationCodeGrant { + authorizationRequestRepository = REQUEST_REPOSITORY + accessTokenResponseClient = CLIENT + } + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Test + fun `oauth2Client when custom authorization request resolver then request resolver used`() { + this.spring.register(RequestResolverConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/callback") { + param("state", "test") + param("code", "123") + } + + verify(RequestResolverConfig.REQUEST_RESOLVER).resolve(any()) + } + + @EnableWebSecurity + open class RequestResolverConfig : WebSecurityConfigurerAdapter() { + companion object { + var REQUEST_RESOLVER: OAuth2AuthorizationRequestResolver = Mockito.mock(OAuth2AuthorizationRequestResolver::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + authorizationCodeGrant { + authorizationRequestResolver = REQUEST_RESOLVER + } + } + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .registrationId("registrationId") + .clientId("clientId") + .clientSecret("clientSecret") + .build() + ) + } + } + + private fun getOAuth2AuthorizationRequest(): OAuth2AuthorizationRequest? { + return OAuth2AuthorizationRequest + .authorizationCode() + .state("test") + .clientId("clientId") + .authorizationUri("https://test") + .redirectUri("http://localhost/callback") + .attributes(mapOf(Pair(OAuth2ParameterNames.REGISTRATION_ID, "registrationId"))) + .build() + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDslTests.kt new file mode 100644 index 0000000000..a7c13e1abe --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDslTests.kt @@ -0,0 +1,144 @@ +/* + * 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.servlet.oauth2.login + +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.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [AuthorizationEndpointDsl] + * + * @author Eleftheria Stein + */ +class AuthorizationEndpointDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Login when custom client registration repository then repository used`() { + this.spring.register(ResolverConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/oauth2/authorization/google") + + verify(ResolverConfig.RESOLVER).resolve(any()) + } + + @EnableWebSecurity + open class ResolverConfig : WebSecurityConfigurerAdapter() { + companion object { + var RESOLVER: OAuth2AuthorizationRequestResolver = Mockito.mock(OAuth2AuthorizationRequestResolver::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Login { + authorizationEndpoint { + authorizationRequestResolver = RESOLVER + } + } + } + } + } + + @Test + fun `oauth2Login when custom authorization request repository then repository used`() { + this.spring.register(RequestRepoConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/oauth2/authorization/google") + + verify(RequestRepoConfig.REPOSITORY).saveAuthorizationRequest(any(), any(), any()) + } + + @EnableWebSecurity + open class RequestRepoConfig : WebSecurityConfigurerAdapter() { + companion object { + var REPOSITORY: AuthorizationRequestRepository = Mockito.mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Login { + authorizationEndpoint { + authorizationRequestRepository = REPOSITORY + } + } + } + } + } + + @Test + fun `oauth2Login when custom authorization uri repository then uri used`() { + this.spring.register(AuthorizationUriConfig::class.java, ClientConfig::class.java).autowire() + + this.mockMvc.get("/connect/google") + + verify(AuthorizationUriConfig.REPOSITORY).saveAuthorizationRequest(any(), any(), any()) + } + + @EnableWebSecurity + open class AuthorizationUriConfig : WebSecurityConfigurerAdapter() { + companion object { + var REPOSITORY: AuthorizationRequestRepository = Mockito.mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + oauth2Login { + authorizationEndpoint { + authorizationRequestRepository = REPOSITORY + baseUri = "/connect" + } + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDslTests.kt new file mode 100644 index 0000000000..0b3dd074bd --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDslTests.kt @@ -0,0 +1,142 @@ +/* + * 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.servlet.oauth2.login + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.core.OAuth2AccessToken +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.security.oauth2.core.user.DefaultOAuth2User +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.* + +/** + * Tests for [RedirectionEndpointDsl] + * + * @author Eleftheria Stein + */ +class RedirectionEndpointDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Login when redirection endpoint configured then custom redirection endpoing used`() { + this.spring.register(UserServiceConfig::class.java, ClientConfig::class.java).autowire() + + val registrationId = "registrationId" + val attributes = HashMap() + attributes[OAuth2ParameterNames.REGISTRATION_ID] = registrationId + val authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("test") + .clientId("clientId") + .authorizationUri("https://test") + .redirectUri("http://localhost/callback") + .attributes(attributes) + .build() + Mockito.`when`(UserServiceConfig.REPOSITORY.removeAuthorizationRequest(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(authorizationRequest) + Mockito.`when`(UserServiceConfig.CLIENT.getTokenResponse(ArgumentMatchers.any())) + .thenReturn(OAuth2AccessTokenResponse + .withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build()) + Mockito.`when`(UserServiceConfig.USER_SERVICE.loadUser(ArgumentMatchers.any())) + .thenReturn(DefaultOAuth2User(listOf(SimpleGrantedAuthority("ROLE_USER")), mapOf(Pair("user", "user")), "user")) + + this.mockMvc.get("/callback") { + param("code", "auth-code") + param("state", "test") + }.andExpect { + redirectedUrl("/") + } + } + + @EnableWebSecurity + open class UserServiceConfig : WebSecurityConfigurerAdapter() { + companion object { + var USER_SERVICE: OAuth2UserService = mock(OAuth2UserService::class.java) as OAuth2UserService + var CLIENT: OAuth2AccessTokenResponseClient = mock(OAuth2AccessTokenResponseClient::class.java) as OAuth2AccessTokenResponseClient + var REPOSITORY: AuthorizationRequestRepository = mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { + userInfoEndpoint { + userService = USER_SERVICE + } + tokenEndpoint { + accessTokenResponseClient = CLIENT + } + authorizationEndpoint { + authorizationRequestRepository = REPOSITORY + } + redirectionEndpoint { + baseUri = "/callback" + } + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .registrationId("registrationId") + .clientId("clientId") + .clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDslTests.kt new file mode 100644 index 0000000000..99dd7fb6bd --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDslTests.kt @@ -0,0 +1,128 @@ +/* + * 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.servlet.oauth2.login + +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.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.core.OAuth2AccessToken +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.* + +/** + * Tests for [TokenEndpointDsl] + * + * @author Eleftheria Stein + */ +class TokenEndpointDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Login when custom access token response client then client used`() { + this.spring.register(TokenConfig::class.java, ClientConfig::class.java).autowire() + + val registrationId = "registrationId" + val attributes = HashMap() + attributes[OAuth2ParameterNames.REGISTRATION_ID] = registrationId + val authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("test") + .clientId("clientId") + .authorizationUri("https://test") + .redirectUri("http://localhost/login/oauth2/code/google") + .attributes(attributes) + .build() + `when`(TokenConfig.REPOSITORY.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest) + `when`(TokenConfig.CLIENT.getTokenResponse(any())).thenReturn(OAuth2AccessTokenResponse + .withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build()) + + this.mockMvc.get("/login/oauth2/code/google") { + param("code", "auth-code") + param("state", "test") + } + + Mockito.verify(TokenConfig.CLIENT).getTokenResponse(any()) + } + + @EnableWebSecurity + open class TokenConfig : WebSecurityConfigurerAdapter() { + companion object { + var CLIENT: OAuth2AccessTokenResponseClient = mock(OAuth2AccessTokenResponseClient::class.java) as OAuth2AccessTokenResponseClient + var REPOSITORY: AuthorizationRequestRepository = mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { + tokenEndpoint { + accessTokenResponseClient = CLIENT + } + authorizationEndpoint { + authorizationRequestRepository = REPOSITORY + } + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .registrationId("registrationId") + .clientId("clientId") + .clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDslTests.kt new file mode 100644 index 0000000000..d100bcf6ed --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDslTests.kt @@ -0,0 +1,139 @@ +/* + * 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.servlet.oauth2.login + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository +import org.springframework.security.oauth2.core.OAuth2AccessToken +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.security.oauth2.core.user.DefaultOAuth2User +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.util.* + +/** + * Tests for [UserInfoEndpointDsl] + * + * @author Eleftheria Stein + */ +class UserInfoEndpointDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `oauth2Login when custom user service then user service used`() { + this.spring.register(UserServiceConfig::class.java, ClientConfig::class.java).autowire() + + val registrationId = "registrationId" + val attributes = HashMap() + attributes[OAuth2ParameterNames.REGISTRATION_ID] = registrationId + val authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("test") + .clientId("clientId") + .authorizationUri("https://test") + .redirectUri("http://localhost/login/oauth2/code/google") + .attributes(attributes) + .build() + `when`(UserServiceConfig.REPOSITORY.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest) + `when`(UserServiceConfig.CLIENT.getTokenResponse(any())) + .thenReturn(OAuth2AccessTokenResponse + .withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build()) + `when`(UserServiceConfig.USER_SERVICE.loadUser(any())) + .thenReturn(DefaultOAuth2User(listOf(SimpleGrantedAuthority("ROLE_USER")), mapOf(Pair("user", "user")), "user")) + + this.mockMvc.get("/login/oauth2/code/google") { + param("code", "auth-code") + param("state", "test") + } + + Mockito.verify(UserServiceConfig.USER_SERVICE).loadUser(any()) + } + + @EnableWebSecurity + open class UserServiceConfig : WebSecurityConfigurerAdapter() { + companion object { + var USER_SERVICE: OAuth2UserService = Mockito.mock(OAuth2UserService::class.java) as OAuth2UserService + var CLIENT: OAuth2AccessTokenResponseClient = Mockito.mock(OAuth2AccessTokenResponseClient::class.java) as OAuth2AccessTokenResponseClient + var REPOSITORY: AuthorizationRequestRepository = Mockito.mock(AuthorizationRequestRepository::class.java) as AuthorizationRequestRepository + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { + userInfoEndpoint { + userService = USER_SERVICE + } + tokenEndpoint { + accessTokenResponseClient = CLIENT + } + authorizationEndpoint { + authorizationRequestRepository = REPOSITORY + } + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .registrationId("registrationId") + .clientId("clientId") + .clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDslTests.kt new file mode 100644 index 0000000000..b168b37eb1 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDslTests.kt @@ -0,0 +1,129 @@ +/* + * 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.servlet.oauth2.resourceserver + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Tests for [JwtDsl] + * + * @author Eleftheria Stein + */ +class JwtDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `JWT when custom JWT decoder then bean not required`() { + this.spring.register(CustomJwtDecoderConfig::class.java).autowire() + } + + @EnableWebSecurity + open class CustomJwtDecoderConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2ResourceServer { + jwt { + jwtDecoder = mock(JwtDecoder::class.java) + } + } + } + } + } + + @Test + fun `JWT when custom jwkSetUri then bean not required`() { + this.spring.register(CustomJwkSetUriConfig::class.java).autowire() + } + + @EnableWebSecurity + open class CustomJwkSetUriConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + oauth2ResourceServer { + jwt { + jwkSetUri = "https://jwk-uri" + } + } + } + } + } + + @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( + Jwt.withTokenValue("token") + .header("alg", "none") + .claim(IdTokenClaimNames.SUB, "user") + .build()) + `when`(CustomJwtAuthenticationConverterConfig.CONVERTER.convert(any())) + .thenReturn(TestingAuthenticationToken("test", "this", "ROLE")) + this.mockMvc.get("/") { + header("Authorization", "Bearer token") + } + + verify(CustomJwtAuthenticationConverterConfig.CONVERTER).convert(any()) + } + + @EnableWebSecurity + open class CustomJwtAuthenticationConverterConfig : WebSecurityConfigurerAdapter() { + companion object { + var CONVERTER: Converter = mock(Converter::class.java) as Converter + var DECODER: JwtDecoder = mock(JwtDecoder::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = CONVERTER + } + } + } + } + + @Bean + open fun jwtDecoder(): JwtDecoder { + return DECODER + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDslTests.kt new file mode 100644 index 0000000000..38dc422599 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDslTests.kt @@ -0,0 +1,148 @@ +/* + * 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.servlet.oauth2.resourceserver + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.http.* +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.Authentication +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal +import org.springframework.security.oauth2.jwt.JwtClaimNames +import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestOperations + +/** + * Tests for [OpaqueTokenDsl] + * + * @author Eleftheria Stein + */ +class OpaqueTokenDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `opaque token when defaults then uses introspection`() { + this.spring.register(DefaultOpaqueConfig::class.java, AuthenticationController::class.java).autowire() + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + val entity = ResponseEntity("{\n" + + " \"active\" : true,\n" + + " \"sub\": \"test-subject\",\n" + + " \"scope\": \"message:read\",\n" + + " \"exp\": 4683883211\n" + + "}", headers, HttpStatus.OK) + `when`(DefaultOpaqueConfig.REST.exchange(any(RequestEntity::class.java), eq(String::class.java))) + .thenReturn(entity) + + this.mockMvc.get("/authenticated") { + header("Authorization", "Bearer token") + }.andExpect { + status { isOk } + content { string("test-subject") } + } + } + + @EnableWebSecurity + open class DefaultOpaqueConfig : WebSecurityConfigurerAdapter() { + companion object { + var REST: RestOperations = mock(RestOperations::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } + } + + @Bean + open fun rest(): RestOperations { + return REST + } + + @Bean + open fun tokenIntrospectionClient(): NimbusOpaqueTokenIntrospector { + return NimbusOpaqueTokenIntrospector("https://example.org/introspect", REST) + } + } + + @Test + fun `opaque token when custom introspector set then introspector used`() { + this.spring.register(CustomIntrospectorConfig::class.java, AuthenticationController::class.java).autowire() + `when`(CustomIntrospectorConfig.INTROSPECTOR.introspect(ArgumentMatchers.anyString())) + .thenReturn(DefaultOAuth2AuthenticatedPrincipal(mapOf(Pair(JwtClaimNames.SUB, "mock-subject")), emptyList())) + + this.mockMvc.get("/authenticated") { + header("Authorization", "Bearer token") + } + + verify(CustomIntrospectorConfig.INTROSPECTOR).introspect("token") + } + + @EnableWebSecurity + open class CustomIntrospectorConfig : WebSecurityConfigurerAdapter() { + companion object { + var INTROSPECTOR: OpaqueTokenIntrospector = mock(OpaqueTokenIntrospector::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = INTROSPECTOR + } + } + } + } + } + + @RestController + class AuthenticationController { + @GetMapping("/authenticated") + fun authenticated(@AuthenticationPrincipal authentication: Authentication): String { + return authentication.name + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDslTests.kt new file mode 100644 index 0000000000..929f7362be --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDslTests.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.servlet.session + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockHttpSession +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.core.session.SessionInformation +import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf +import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.* + +/** + * Tests for [SessionConcurrencyDsl] + * + * @author Eleftheria Stein + */ +class SessionConcurrencyDslTests { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `session concurrency when maximum sessions then no more sessions allowed`() { + this.spring.register(MaximumSessionsConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")) + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/login?error")) + } + + @EnableWebSecurity + open class MaximumSessionsConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionConcurrency { + maximumSessions = 1 + maxSessionsPreventsLogin = true + } + } + formLogin { } + } + } + } + + @Test + fun `session concurrency when expired url then redirects to url`() { + this.spring.register(ExpiredUrlConfig::class.java).autowire() + + val session = MockHttpSession() + val sessionInformation = SessionInformation("", session.id, Date(0)) + sessionInformation.expireNow() + `when`(ExpiredUrlConfig.sessionRegistry.getSessionInformation(any())).thenReturn(sessionInformation) + + this.mockMvc.perform(get("/").session(session)) + .andExpect(redirectedUrl("/expired-session")) + } + + @EnableWebSecurity + open class ExpiredUrlConfig : WebSecurityConfigurerAdapter() { + companion object { + val sessionRegistry: SessionRegistry = mock(SessionRegistry::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionConcurrency { + maximumSessions = 1 + expiredUrl = "/expired-session" + sessionRegistry = sessionRegistry() + } + } + } + } + + @Bean + open fun sessionRegistry(): SessionRegistry { + return sessionRegistry + } + } + + @Test + fun `session concurrency when expired session strategy then strategy used`() { + this.spring.register(ExpiredSessionStrategyConfig::class.java).autowire() + + val session = MockHttpSession() + val sessionInformation = SessionInformation("", session.id, Date(0)) + sessionInformation.expireNow() + `when`(ExpiredSessionStrategyConfig.sessionRegistry.getSessionInformation(any())).thenReturn(sessionInformation) + + this.mockMvc.perform(get("/").session(session)) + .andExpect(redirectedUrl("/expired-session")) + } + + @EnableWebSecurity + open class ExpiredSessionStrategyConfig : WebSecurityConfigurerAdapter() { + companion object { + val sessionRegistry: SessionRegistry = mock(SessionRegistry::class.java) + } + + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionConcurrency { + maximumSessions = 1 + expiredSessionStrategy = SimpleRedirectSessionInformationExpiredStrategy("/expired-session") + sessionRegistry = sessionRegistry() + } + } + } + } + + @Bean + open fun sessionRegistry(): SessionRegistry { + return sessionRegistry + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt new file mode 100644 index 0000000000..8624d24009 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt @@ -0,0 +1,195 @@ +/* + * 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.servlet.session + +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.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockHttpSession +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders + +/** + * Tests for [SessionFixationDsl] + * + * @author Eleftheria Stein + */ +class SessionFixationDslTest { + @Rule + @JvmField + var spring = SpringTestRule() + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `session fixation when strategy is new session then new session created and attributes are not preserved`() { + this.spring.register(NewSessionConfig::class.java, UserDetailsConfig::class.java).autowire() + val givenSession = MockHttpSession() + val givenSessionId = givenSession.id + givenSession.clearAttributes() + givenSession.setAttribute("name", "value") + + val result = this.mockMvc.perform(MockMvcRequestBuilders.get("/") + .with(httpBasic("user", "password")) + .session(givenSession)) + .andReturn() + + val resultingSession = result.request.getSession(false) + assertThat(resultingSession).isNotEqualTo(givenSession) + assertThat(resultingSession!!.id).isNotEqualTo(givenSessionId) + assertThat(resultingSession.getAttribute("name")).isNull() + } + + @EnableWebSecurity + open class NewSessionConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionFixation { + newSession() + } + } + httpBasic { } + } + } + } + + @Test + fun `session fixation when strategy is migrate session then new session created and attributes are preserved`() { + this.spring.register(MigrateSessionConfig::class.java, UserDetailsConfig::class.java).autowire() + val givenSession = MockHttpSession() + val givenSessionId = givenSession.id + givenSession.clearAttributes() + givenSession.setAttribute("name", "value") + + val result = this.mockMvc.perform(MockMvcRequestBuilders.get("/") + .with(httpBasic("user", "password")) + .session(givenSession)) + .andReturn() + + val resultingSession = result.request.getSession(false) + assertThat(resultingSession).isNotEqualTo(givenSession) + assertThat(resultingSession!!.id).isNotEqualTo(givenSessionId) + assertThat(resultingSession.getAttribute("name")).isEqualTo("value") + } + + @EnableWebSecurity + open class MigrateSessionConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionFixation { + migrateSession() + } + } + httpBasic { } + } + } + } + + @Test + fun `session fixation when strategy is change session id then session id changes and attributes preserved`() { + this.spring.register(ChangeSessionIdConfig::class.java, UserDetailsConfig::class.java).autowire() + val givenSession = MockHttpSession() + val givenSessionId = givenSession.id + givenSession.clearAttributes() + givenSession.setAttribute("name", "value") + + val result = this.mockMvc.perform(MockMvcRequestBuilders.get("/") + .with(httpBasic("user", "password")) + .session(givenSession)) + .andReturn() + + val resultingSession = result.request.getSession(false) + assertThat(resultingSession).isEqualTo(givenSession) + assertThat(resultingSession!!.id).isNotEqualTo(givenSessionId) + assertThat(resultingSession.getAttribute("name")).isEqualTo("value") + } + + @EnableWebSecurity + open class ChangeSessionIdConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionFixation { + changeSessionId() + } + } + httpBasic { } + } + } + } + + @Test + fun `session fixation when strategy is none then session does not change`() { + this.spring.register(NoneConfig::class.java, UserDetailsConfig::class.java).autowire() + val givenSession = MockHttpSession() + val givenSessionId = givenSession.id + givenSession.clearAttributes() + givenSession.setAttribute("name", "value") + + val result = this.mockMvc.perform(MockMvcRequestBuilders.get("/") + .with(httpBasic("user", "password")) + .session(givenSession)) + .andReturn() + + val resultingSession = result.request.getSession(false) + assertThat(resultingSession).isEqualTo(givenSession) + assertThat(resultingSession!!.id).isEqualTo(givenSessionId) + assertThat(resultingSession.getAttribute("name")).isEqualTo("value") + } + + @EnableWebSecurity + open class NoneConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + sessionManagement { + sessionFixation { + none() + } + } + httpBasic { } + } + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } + } +} diff --git a/gradle.properties b/gradle.properties index 7c72109b0d..71ff0f8305 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ gaeVersion=1.9.76 springBootVersion=2.2.0.RELEASE version=5.3.0.BUILD-SNAPSHOT org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError +kotlinVersion=1.3.61 diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index 2d76fd3a2d..4add9f167c 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -9,6 +9,9 @@ if (!project.hasProperty("springVersion")) { if (!project.hasProperty("springDataVersion")) { ext.springDataVersion = "Moore-SR+" } +if (!project.hasProperty("kotlinVersion")) { + ext.kotlinVersion = "1.3.61" +} ext.rsocketVersion = "1.+" ext.openSamlVersion = "3.+" @@ -29,6 +32,7 @@ dependencies { management platform("org.springframework:spring-framework-bom:$springVersion") management platform("io.projectreactor:reactor-bom:$reactorVersion") management platform("org.springframework.data:spring-data-releasetrain:$springDataVersion") + management platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") constraints { management "ch.qos.logback:logback-classic:1.+" management "com.fasterxml.jackson.core:jackson-databind:2.+" diff --git a/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts b/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts new file mode 100644 index 0000000000..4d3409c7d6 --- /dev/null +++ b/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts @@ -0,0 +1,31 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("io.spring.convention.spring-sample-boot") + kotlin("jvm") + kotlin("plugin.spring") version "1.3.61" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":spring-security-core")) + implementation(project(":spring-security-config")) + implementation(project(":spring-security-web")) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation(project(":spring-security-test")) + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "1.8" + } +} diff --git a/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/KotlinApplication.kt b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/KotlinApplication.kt new file mode 100644 index 0000000000..1d7c7b28f2 --- /dev/null +++ b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/KotlinApplication.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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.samples + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +/** + * @author Eleftheria Stein + */ +@SpringBootApplication +class KotlinApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt new file mode 100644 index 0000000000..043f29ecae --- /dev/null +++ b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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.samples.config + +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.web.servlet.invoke +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager + +/** + * @author Eleftheria Stein + */ +@EnableWebSecurity +class SecurityConfig : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/css/**", permitAll) + authorize("/user/**", hasAuthority("ROLE_USER")) + } + formLogin { + loginPage = "/log-in" + } + } + } + + @Bean + public override fun userDetailsService(): UserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(userDetails) + } +} diff --git a/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/web/MainController.kt b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/web/MainController.kt new file mode 100644 index 0000000000..e8c8128ad8 --- /dev/null +++ b/samples/boot/kotlin/src/main/kotlin/org/springframework/security/samples/web/MainController.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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.samples.web + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +/** + * @author Eleftheria Stein + */ +@Controller +class MainController { + + @GetMapping("/") + fun index(): String { + return "index" + } + + @GetMapping("/user/index") + fun userIndex(): String { + return "user/index" + } + + @GetMapping("/log-in") + fun login(): String { + return "login" + } +} diff --git a/samples/boot/kotlin/src/main/resources/application.yml b/samples/boot/kotlin/src/main/resources/application.yml new file mode 100644 index 0000000000..8c01e005bc --- /dev/null +++ b/samples/boot/kotlin/src/main/resources/application.yml @@ -0,0 +1,6 @@ +server: + port: 8080 + +spring: + thymeleaf: + cache: false diff --git a/samples/boot/kotlin/src/main/resources/static/css/main.css b/samples/boot/kotlin/src/main/resources/static/css/main.css new file mode 100644 index 0000000000..de0941ecd5 --- /dev/null +++ b/samples/boot/kotlin/src/main/resources/static/css/main.css @@ -0,0 +1,8 @@ +body { + font-family: sans; + font-size: 1em; +} + +div.logout { + float: right; +} diff --git a/samples/boot/kotlin/src/main/resources/templates/index.html b/samples/boot/kotlin/src/main/resources/templates/index.html new file mode 100644 index 0000000000..c30f4a8292 --- /dev/null +++ b/samples/boot/kotlin/src/main/resources/templates/index.html @@ -0,0 +1,40 @@ + + + + + + Hello Spring Security + + + + +

+ Logged in user: | + Roles: +
+
+ +
+
+
+

Hello Spring Security

+

This is an unsecured page, but you can access the secured pages after authenticating.

+ + + diff --git a/samples/boot/kotlin/src/main/resources/templates/login.html b/samples/boot/kotlin/src/main/resources/templates/login.html new file mode 100644 index 0000000000..2ee9216937 --- /dev/null +++ b/samples/boot/kotlin/src/main/resources/templates/login.html @@ -0,0 +1,20 @@ + + + + Login page + + + + +

Login page

+

Example user: user / password

+
+ : +
+ : +
+ +
+

Back to home page

+ + diff --git a/samples/boot/kotlin/src/main/resources/templates/user/index.html b/samples/boot/kotlin/src/main/resources/templates/user/index.html new file mode 100644 index 0000000000..b36caed027 --- /dev/null +++ b/samples/boot/kotlin/src/main/resources/templates/user/index.html @@ -0,0 +1,29 @@ + + + + + + Hello Spring Security + + + + +
+

This is a secured page!

+

Back to home page

+ + diff --git a/samples/boot/kotlin/src/test/kotlin/org/springframework/security/samples/KotlinApplicationTests.kt b/samples/boot/kotlin/src/test/kotlin/org/springframework/security/samples/KotlinApplicationTests.kt new file mode 100644 index 0000000000..e3fea7da73 --- /dev/null +++ b/samples/boot/kotlin/src/test/kotlin/org/springframework/security/samples/KotlinApplicationTests.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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.samples + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.mock.web.MockHttpSession +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin +import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated +import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@RunWith(SpringRunner::class) +@SpringBootTest +@AutoConfigureMockMvc +class KotlinApplicationTests { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + fun `index page is not protected`() { + this.mockMvc.get("/") + .andExpect { + status { isOk } + } + } + + @Test + fun `protected page redirects to login`() { + val mvcResult = this.mockMvc.get("/user/index") + .andExpect { status { is3xxRedirection } } + .andReturn() + + assertThat(mvcResult.response.redirectedUrl).endsWith("/log-in") + } + + @Test + fun `valid user permitted to log in`() { + this.mockMvc.perform(formLogin("/log-in").user("user").password("password")) + .andExpect(authenticated()) + } + + @Test + fun `invalid user not permitted to log in`() { + this.mockMvc.perform(formLogin("/log-in").user("invalid").password("invalid")) + .andExpect(unauthenticated()) + .andExpect(status().is3xxRedirection) + } + + @Test + fun `logged in user can access protected page`() { + val mvcResult = this.mockMvc.perform(formLogin("/log-in").user("user").password("password")) + .andExpect(authenticated()).andReturn() + + val httpSession = mvcResult.request.getSession(false) as MockHttpSession + + this.mockMvc.get("/user/index") { + session = httpSession + }.andExpect { + status { isOk } + } + } +} diff --git a/settings.gradle b/settings.gradle index cb397f5e87..30e1e1a572 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,7 @@ rootProject.name = 'spring-security' FileTree buildFiles = fileTree(rootDir) { List excludes = gradle.startParameter.projectProperties.get("excludeProjects")?.split(",") - include '**/*.gradle' + include '**/*.gradle', '**/*.gradle.kts' exclude 'build', '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' exclude '**/grails3' if(excludes) { @@ -14,12 +14,18 @@ String rootDirPath = rootDir.absolutePath + File.separator buildFiles.each { File buildFile -> boolean isDefaultName = 'build.gradle'.equals(buildFile.name) + boolean isKotlin = buildFile.name.endsWith(".kts") if(isDefaultName) { String buildFilePath = buildFile.parentFile.absolutePath String projectPath = buildFilePath.replace(rootDirPath, '').replace(File.separator, ':') include projectPath } else { - String projectName = buildFile.name.replace('.gradle', ''); + String projectName + if (isKotlin) { + projectName = buildFile.name.replace('.gradle.kts', '') + } else { + projectName = buildFile.name.replace('.gradle', '') + } String projectPath = ':' + projectName; include projectPath def project = findProject("${projectPath}")