Idiomatic Kotlin DSL for server HTTP security

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

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import java.util.*
/**
* Tests for [AuthorizeExchangeDsl]
*
* @author Eleftheria Stein
*/
class AuthorizeExchangeDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when secured by matcher then responds with unauthorized`() {
this.spring.register(MatcherAuthenticatedConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized
}
@EnableWebFluxSecurity
@EnableWebFlux
open class MatcherAuthenticatedConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
}
@Test
fun `request when allowed by matcher then responds with ok`() {
this.spring.register(MatcherPermitAllConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class MatcherPermitAllConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, permitAll)
}
}
}
@RestController
internal class PathController {
@RequestMapping("/")
fun path() {
}
}
}
@Test
fun `request when secured by pattern then responds with unauthorized`() {
this.spring.register(PatternAuthenticatedConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized
}
@Test
fun `request when allowed by pattern then responds with ok`() {
this.spring.register(PatternAuthenticatedConfig::class.java).autowire()
this.client.get()
.uri("/public")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PatternAuthenticatedConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/public", permitAll)
authorize("/**", authenticated)
}
}
}
@RestController
internal class PathController {
@RequestMapping("/public")
fun public() {
}
}
}
@Test
fun `request when missing required role then responds with forbidden`() {
this.spring.register(HasRoleConfig::class.java).autowire()
this.client
.get()
.uri("/")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
.exchange()
.expectStatus().isForbidden
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HasRoleConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, hasRole("ADMIN"))
}
httpBasic { }
}
}
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
/**
* Tests for [ServerAnonymousDsl]
*
* @author Eleftheria Stein
*/
class ServerAnonymousDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `authentication when anonymous enabled then is of type anonymous authentication`() {
this.spring.register(AnonymousConfig::class.java, HttpMeController::class.java).autowire()
this.client.get()
.uri("/principal")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("anonymousUser")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AnonymousConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
anonymous { }
}
}
}
@Test
fun `anonymous when custom principal specified then custom principal is used`() {
this.spring.register(CustomPrincipalConfig::class.java, HttpMeController::class.java).autowire()
this.client.get()
.uri("/principal")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("anon")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomPrincipalConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
anonymous {
principal = "anon"
}
}
}
}
@Test
fun `anonymous when disabled then principal is null`() {
this.spring.register(AnonymousDisabledConfig::class.java, HttpMeController::class.java).autowire()
this.client.get()
.uri("/principal")
.exchange()
.expectStatus().isOk
.expectBody<String>().consumeWith { body -> assertThat(body.responseBody).isNull() }
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AnonymousDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
anonymous {
disable()
}
}
}
}
@Test
fun `anonymous when custom key specified then custom key used`() {
this.spring.register(CustomKeyConfig::class.java, HttpMeController::class.java).autowire()
this.client.get()
.uri("/key")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("key".hashCode().toString())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomKeyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
anonymous {
key = "key"
}
}
}
}
@Test
fun `anonymous when custom authorities specified then custom authorities used`() {
this.spring.register(CustomAuthoritiesConfig::class.java, HttpMeController::class.java).autowire()
this.client.get()
.uri("/principal")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomAuthoritiesConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
anonymous {
authorities = listOf(SimpleGrantedAuthority("TEST"))
}
authorizeExchange {
authorize(anyExchange, hasAuthority("TEST"))
}
}
}
}
@RestController
class HttpMeController {
@GetMapping("/principal")
fun principal(@AuthenticationPrincipal principal: String?): String? {
return principal
}
@GetMapping("/key")
fun key(@AuthenticationPrincipal principal: Mono<AnonymousAuthenticationToken>): Mono<String> {
return principal
.map { it.keyHash }
.map { it.toString() }
}
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.CorsConfigurationSource
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerCorsDsl]
*
* @author Eleftheria Stein
*/
class ServerCorsDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when CORS configured using bean then Access-Control-Allow-Origin header in response`() {
this.spring.register(CorsBeanConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.header(HttpHeaders.ORIGIN, "https://origin.example.com")
.exchange()
.expectHeader().valueEquals("Access-Control-Allow-Origin", "*")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CorsBeanConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
cors { }
}
}
@Bean
open fun corsConfigurationSource(): CorsConfigurationSource {
val source = UrlBasedCorsConfigurationSource()
val corsConfiguration = CorsConfiguration()
corsConfiguration.allowedOrigins = listOf("*")
source.registerCorsConfiguration("/**", corsConfiguration)
return source
}
}
@Test
fun `request when CORS configured using source then Access-Control-Allow-Origin header in response`() {
this.spring.register(CorsSourceConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.header(HttpHeaders.ORIGIN, "https://origin.example.com")
.exchange()
.expectHeader().valueEquals("Access-Control-Allow-Origin", "*")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CorsSourceConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val source = UrlBasedCorsConfigurationSource()
val corsConfiguration = CorsConfiguration()
corsConfiguration.allowedOrigins = listOf("*")
source.registerCorsConfiguration("/**", corsConfiguration)
return http {
cors {
configurationSource = source
}
}
}
}
@Test
fun `request when CORS disabled then no Access-Control-Allow-Origin header in response`() {
this.spring.register(CorsDisabledConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.header(HttpHeaders.ORIGIN, "https://origin.example.com")
.exchange()
.expectHeader().doesNotExist("Access-Control-Allow-Origin")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CorsDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
cors {
disable()
}
}
}
}
}

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerCsrfDsl]
*
* @author Eleftheria Stein
*/
class ServerCsrfDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `post when CSRF protection enabled then requires CSRF token`() {
this.spring.register(CsrfConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
.expectStatus().isForbidden
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CsrfConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf { }
}
}
}
@Test
fun `post when CSRF protection disabled then CSRF token is not required`() {
this.spring.register(CsrfDisabledConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CsrfDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf {
disable()
}
}
}
@RestController
internal class TestController {
@PostMapping("/")
fun home() {
}
}
}
@Test
fun `post when request matches CSRF matcher then CSRF token required`() {
this.spring.register(CsrfMatcherConfig::class.java).autowire()
this.client.post()
.uri("/csrf")
.exchange()
.expectStatus().isForbidden
}
@Test
fun `post when request does not match CSRF matcher then CSRF token is not required`() {
this.spring.register(CsrfMatcherConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CsrfMatcherConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf {
requireCsrfProtectionMatcher = PathPatternParserServerWebExchangeMatcher("/csrf")
}
}
}
@RestController
internal class TestController {
@PostMapping("/")
fun home() {
}
@PostMapping("/csrf")
fun csrf() {
}
}
}
@Test
fun `csrf when custom access denied handler then handler used`() {
this.spring.register(CustomAccessDeniedHandlerConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
Mockito.verify<ServerAccessDeniedHandler>(CustomAccessDeniedHandlerConfig.ACCESS_DENIED_HANDLER)
.handle(any(), any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomAccessDeniedHandlerConfig {
companion object {
var ACCESS_DENIED_HANDLER: ServerAccessDeniedHandler = mock(ServerAccessDeniedHandler::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf {
accessDeniedHandler = ACCESS_DENIED_HANDLER
}
}
}
}
@Test
fun `csrf when custom token repository then repository used`() {
this.spring.register(CustomCsrfTokenRepositoryConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
Mockito.verify<ServerCsrfTokenRepository>(CustomCsrfTokenRepositoryConfig.TOKEN_REPOSITORY)
.loadToken(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomCsrfTokenRepositoryConfig {
companion object {
var TOKEN_REPOSITORY: ServerCsrfTokenRepository = mock(ServerCsrfTokenRepository::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf {
csrfTokenRepository = TOKEN_REPOSITORY
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpStatus
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import java.util.*
/**
* Tests for [ServerExceptionHandlingDsl]
*
* @author Eleftheria Stein
*/
class ServerExceptionHandlingDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `unauthenticated request when custom entry point then directed to custom entry point`() {
this.spring.register(EntryPointConfig::class.java).autowire()
val result = this.client.get()
.uri("/")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
Assertions.assertThat(result.responseHeaders.location).hasPath("/auth")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class EntryPointConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
exceptionHandling {
authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth")
}
}
}
}
@Test
fun `unauthorized request when custom access denied handler then directed to custom access denied handler`() {
this.spring.register(AccessDeniedHandlerConfig::class.java).autowire()
this.client
.get()
.uri("/")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
.exchange()
.expectStatus().isSeeOther
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AccessDeniedHandlerConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, hasRole("ADMIN"))
}
httpBasic { }
exceptionHandling {
accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER)
}
}
}
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
}

View File

@@ -0,0 +1,325 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
import org.springframework.security.web.server.context.ServerSecurityContextRepository
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.test.web.reactive.server.FluxExchangeResult
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.function.BodyInserters
/**
* Tests for [ServerFormLoginDsl]
*
* @author Eleftheria Stein
*/
class ServerFormLoginDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when form login enabled then redirects to default login page`() {
this.spring.register(DefaultFormLoginConfig::class.java, UserDetailsConfig::class.java).autowire()
val result: FluxExchangeResult<String> = this.client.get()
.uri("/")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/login")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class DefaultFormLoginConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
}
}
}
@Test
fun `request when custom login page then redirects to custom login page`() {
this.spring.register(CustomLoginPageConfig::class.java, UserDetailsConfig::class.java).autowire()
val result: FluxExchangeResult<String> = this.client.get()
.uri("/")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/log-in")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomLoginPageConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin {
loginPage = "/log-in"
}
}
}
}
@Test
fun `form login when custom authentication manager then manager used`() {
this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
this.client
.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
verify<ReactiveAuthenticationManager>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER)
.authenticate(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomAuthenticationManagerConfig {
companion object {
var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = Mockito.mock(ReactiveAuthenticationManager::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin {
authenticationManager = AUTHENTICATION_MANAGER
}
}
}
}
@Test
fun `form login when custom authentication entry point then entry point used`() {
this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
val result = this.client.get()
.uri("/")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/entry")
}
}
@Test
fun `form login when custom requires authentication matcher then matching request logs in`() {
this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
val result = this.client
.mutateWith(csrf())
.post()
.uri("/log-in")
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/")
}
}
@Test
fun `invalid login when custom failure handler then failure handler used`() {
this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire()
val result = this.client
.mutateWith(csrf())
.post()
.uri("/log-in")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/log-in-error")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin {
authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/entry")
requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/log-in")
authenticationFailureHandler = RedirectServerAuthenticationFailureHandler("/log-in-error")
}
}
}
}
@Test
fun `login when custom success handler then success handler used`() {
this.spring.register(CustomSuccessHandlerConfig::class.java, UserDetailsConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
val result = this.client
.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasPath("/success")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomSuccessHandlerConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin {
authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/success")
}
}
}
}
@Test
fun `form login when custom security context repository then repository used`() {
this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
this.client
.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
verify<ServerSecurityContextRepository>(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY)
.save(Mockito.any(), Mockito.any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomSecurityContextRepositoryConfig {
companion object {
var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = Mockito.mock(ServerSecurityContextRepository::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin {
securityContextRepository = SECURITY_CONTEXT_REPOSITORY
}
}
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerHeadersDsl]
*
* @author Eleftheria Stein
*/
class ServerHeadersDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when default headers configured then default headers are in the response`() {
this.spring.register(DefaultHeadersConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
.expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class DefaultHeadersConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers { }
}
}
}
@Test
fun `request when headers disabled then no security headers are in the response`() {
this.spring.register(HeadersDisabledConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS)
.expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS)
.expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY)
.expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL)
.expectHeader().doesNotExist(HttpHeaders.EXPIRES)
.expectHeader().doesNotExist(HttpHeaders.PRAGMA)
.expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HeadersDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
disable()
}
}
}
}
@Test
fun `request when feature policy configured then feature policy header in response`() {
this.spring.register(FeaturePolicyConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals("Feature-Policy", "geolocation 'self'")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class FeaturePolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
featurePolicy("geolocation 'self'")
}
}
}
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.BDDMockito.given
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
import org.springframework.security.web.server.context.ServerSecurityContextRepository
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
import java.util.*
/**
* Tests for [ServerHttpBasicDsl]
*
* @author Eleftheria Stein
*/
class ServerHttpBasicDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `http basic when no authorization header then responds with unauthorized`() {
this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized
}
@Test
fun `http basic when valid authorization header then responds with ok`() {
this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire()
this.client.get()
.uri("/")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HttpBasicConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic { }
}
}
@RestController
internal class PathController {
@RequestMapping("/")
fun path() {
}
}
}
@Test
fun `http basic when custom authentication manager then manager used`() {
given<Mono<Authentication>>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any()))
.willReturn(Mono.just<Authentication>(TestingAuthenticationToken("user", "password", "ROLE_USER")))
this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire()
this.client.get()
.uri("/")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
.exchange()
verify<ReactiveAuthenticationManager>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER)
.authenticate(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomAuthenticationManagerConfig {
companion object {
var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic {
authenticationManager = AUTHENTICATION_MANAGER
}
}
}
}
@Test
fun `http basic when custom security context repository then repository used`() {
this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire()
this.client.get()
.uri("/")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray()))
.exchange()
verify<ServerSecurityContextRepository>(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY)
.save(any(), any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomSecurityContextRepositoryConfig {
companion object {
var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = mock(ServerSecurityContextRepository::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic {
securityContextRepository = SECURITY_CONTEXT_REPOSITORY
}
}
}
}
@Test
fun `http basic when custom authentication entry point then entry point used`() {
this.spring.register(CustomAuthenticationEntryPointConfig::class.java, UserDetailsConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
verify<ServerAuthenticationEntryPoint>(CustomAuthenticationEntryPointConfig.ENTRY_POINT)
.commence(any(), any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomAuthenticationEntryPointConfig {
companion object {
var ENTRY_POINT: ServerAuthenticationEntryPoint = mock(ServerAuthenticationEntryPoint::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic {
authenticationEntryPoint = ENTRY_POINT
}
}
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerHttpSecurityDsl]
*
* @author Eleftheria Stein
*/
class ServerHttpSecurityDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when it does not match the security matcher then the security rules do not apply`() {
this.spring.register(PatternMatcherConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isNotFound
}
@Test
fun `request when it matches the security matcher then the security rules apply`() {
this.spring.register(PatternMatcherConfig::class.java).autowire()
this.client.get()
.uri("/api")
.exchange()
.expectStatus().isUnauthorized
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PatternMatcherConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**"))
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
}
@Test
fun `post when default security configured then CSRF prevents the request`() {
this.spring.register(DefaultSecurityConfig::class.java).autowire()
this.client.post()
.uri("/")
.exchange()
.expectStatus().isForbidden
}
@Test
fun `request when default security configured then default headers are in the response`() {
this.spring.register(DefaultSecurityConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
.expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class DefaultSecurityConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
}
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.PortMapperImpl
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import java.util.*
/**
* Tests for [ServerHttpsRedirectDsl]
*
* @author Eleftheria Stein
*/
class ServerHttpsRedirectDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() {
this.spring.register(HttpRedirectMatcherConfig::class.java).autowire()
val result = this.client.get()
.uri("/secure")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasScheme("https")
}
}
@Test
fun `request when does not match redirect to HTTPS matcher then does not redirect`() {
this.spring.register(HttpRedirectMatcherConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isNotFound
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HttpRedirectMatcherConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
redirectToHttps {
httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure"))
}
}
}
}
@Test
fun `request when matches redirect to HTTPS function then redirects to HTTPS`() {
this.spring.register(HttpRedirectFunctionConfig::class.java).autowire()
val result = this.client.get()
.uri("/")
.header("X-Requires-Https", "required")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasScheme("https")
}
}
@Test
fun `request when does not match redirect to HTTPS function then does not redirect`() {
this.spring.register(HttpRedirectFunctionConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isNotFound
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HttpRedirectFunctionConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
redirectToHttps {
httpsRedirectWhen {
it.request.headers.containsKey("X-Requires-Https")
}
}
}
}
}
@Test
fun `request when multiple rules configured then only the last rule applies`() {
this.spring.register(HttpRedirectMatcherAndFunctionConfig::class.java).autowire()
this.client.get()
.uri("/secure")
.exchange()
.expectStatus().isNotFound
val result = this.client.get()
.uri("/")
.header("X-Requires-Https", "required")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasScheme("https")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HttpRedirectMatcherAndFunctionConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
redirectToHttps {
httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure"))
httpsRedirectWhen {
it.request.headers.containsKey("X-Requires-Https")
}
}
}
}
}
@Test
fun `request when port mapper configured then redirected to HTTPS port`() {
this.spring.register(PortMapperConfig::class.java).autowire()
val result = this.client.get()
.uri("http://localhost:543")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location).hasScheme("https").hasPort(123)
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PortMapperConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val customPortMapper = PortMapperImpl()
customPortMapper.setPortMappings(Collections.singletonMap("543", "123"))
return http {
redirectToHttps {
portMapper = customPortMapper
}
}
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
/**
* Tests for [ServerLogoutDsl]
*
* @author Eleftheria Stein
*/
class ServerLogoutDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `logout when defaults used then redirects to login page`() {
this.spring.register(LogoutConfig::class.java).autowire()
val result = this.client
.mutateWith(csrf())
.post()
.uri("/logout")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location)
.hasPath("/login")
.hasParameter("logout")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class LogoutConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
logout { }
}
}
}
@Test
fun `logout when custom logout URL then custom URL redirects to login page`() {
this.spring.register(CustomUrlConfig::class.java).autowire()
val result = this.client
.mutateWith(csrf())
.post()
.uri("/custom-logout")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location)
.hasPath("/login")
.hasParameter("logout")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomUrlConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
logout {
logoutUrl = "/custom-logout"
}
}
}
}
@Test
fun `logout when custom requires logout matcher then matching request redirects to login page`() {
this.spring.register(RequiresLogoutConfig::class.java).autowire()
val result = this.client
.mutateWith(csrf())
.post()
.uri("/custom-logout")
.exchange()
.expectStatus().is3xxRedirection
.returnResult(String::class.java)
result.assertWithDiagnostics {
assertThat(result.responseHeaders.location)
.hasPath("/login")
.hasParameter("logout")
}
}
@EnableWebFluxSecurity
@EnableWebFlux
open class RequiresLogoutConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
logout {
requiresLogout = PathPatternParserServerWebExchangeMatcher("/custom-logout")
}
}
}
}
@Test
fun `logout when custom logout handler then custom handler invoked`() {
this.spring.register(CustomLogoutHandlerConfig::class.java).autowire()
`when`(CustomLogoutHandlerConfig.LOGOUT_HANDLER.logout(any(), any()))
.thenReturn(Mono.empty())
this.client
.mutateWith(csrf())
.post()
.uri("/logout")
.exchange()
verify<ServerLogoutHandler>(CustomLogoutHandlerConfig.LOGOUT_HANDLER)
.logout(any(), any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomLogoutHandlerConfig {
companion object {
var LOGOUT_HANDLER: ServerLogoutHandler = mock(ServerLogoutHandler::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
logout {
logoutHandler = LOGOUT_HANDLER
}
}
}
}
@Test
fun `logout when custom logout success handler then custom handler invoked`() {
this.spring.register(CustomLogoutSuccessHandlerConfig::class.java).autowire()
this.client
.mutateWith(csrf())
.post()
.uri("/logout")
.exchange()
verify<ServerLogoutSuccessHandler>(CustomLogoutSuccessHandlerConfig.LOGOUT_HANDLER)
.onLogoutSuccess(any(), any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomLogoutSuccessHandlerConfig {
companion object {
var LOGOUT_HANDLER: ServerLogoutSuccessHandler = mock(ServerLogoutSuccessHandler::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
logout {
logoutSuccessHandler = LOGOUT_HANDLER
}
}
}
}
@Test
fun `logout when disabled then logout URL not found`() {
this.spring.register(LogoutDisabledConfig::class.java).autowire()
this.client
.mutateWith(csrf())
.post()
.uri("/logout")
.exchange()
.expectStatus().isNotFound
}
@EnableWebFluxSecurity
@EnableWebFlux
open class LogoutDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, permitAll)
}
logout {
disable()
}
}
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
/**
* Tests for [ServerOAuth2ClientDsl]
*
* @author Eleftheria Stein
*/
class ServerOAuth2ClientDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `OAuth2 client when custom client registration repository then bean is not required`() {
this.spring.register(ClientRepoConfig::class.java).autowire()
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ClientRepoConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Client {
clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE
.getBuilder("google").clientId("clientId").clientSecret("clientSecret")
.build()
)
}
}
}
}
@Test
fun `OAuth2 client when authorization request repository configured then custom repository used`() {
this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
this.client.get()
.uri {
it.path("/")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build()
}
.exchange()
verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).loadAuthorizationRequest(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthorizationRequestRepositoryConfig {
companion object {
var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Client {
authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
}
}
}
}
@Test
fun `OAuth2 client when authentication converter configured then custom converter used`() {
this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire()
`when`(AuthenticationConverterConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/login/oauth/authorize")
.clientId("clientId")
.redirectUri("/authorize/oauth2/code/google")
.build()))
this.client.get()
.uri {
it.path("/authorize/oauth2/code/google")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build()
}
.exchange()
verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationConverterConfig {
companion object {
var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Client {
authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
authenticationConverter = AUTHENTICATION_CONVERTER
}
}
}
}
@Test
fun `OAuth2 client when authentication manager configured then custom manager used`() {
this.spring.register(AuthenticationManagerConfig::class.java, ClientConfig::class.java).autowire()
`when`(AuthenticationManagerConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://example.com/login/oauth/authorize")
.clientId("clientId")
.redirectUri("/authorize/oauth2/code/google")
.build()))
`when`(AuthenticationManagerConfig.AUTHENTICATION_CONVERTER.convert(any()))
.thenReturn(Mono.just(TestingAuthenticationToken("a", "b", "c")))
this.client.get()
.uri {
it.path("/authorize/oauth2/code/google")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build()
}
.exchange()
verify(AuthenticationManagerConfig.AUTHENTICATION_MANAGER).authenticate(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationManagerConfig {
companion object {
var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Client {
authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
authenticationConverter = AUTHENTICATION_CONVERTER
authenticationManager = AUTHENTICATION_MANAGER
}
}
}
}
@Configuration
open class ClientConfig {
@Bean
open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository {
return InMemoryReactiveClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE
.getBuilder("google").clientId("clientId").clientSecret("clientSecret")
.build()
)
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerOAuth2LoginDsl]
*
* @author Eleftheria Stein
*/
class ServerOAuth2LoginDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `oauth2Login when custom client registration repository then bean is not required`() {
this.spring.register(ClientRepoConfig::class.java).autowire()
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ClientRepoConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login {
clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE
.getBuilder("google").clientId("clientId").clientSecret("clientSecret")
.build()
)
}
}
}
}
@Test
fun `login page when OAuth2 login configured then default login page created`() {
this.spring.register(OAuth2LoginConfig::class.java, ClientConfig::class.java).autowire()
this.client.get()
.uri("/login")
.exchange()
.expectStatus().isOk
}
@EnableWebFluxSecurity
@EnableWebFlux
open class OAuth2LoginConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Login { }
}
}
}
@Test
fun `OAuth2 login when authorization request repository configured then custom repository used`() {
this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
this.client.get()
.uri("/login/oauth2/code/google")
.exchange()
verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).removeAuthorizationRequest(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthorizationRequestRepositoryConfig {
companion object {
var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java)
as ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest>
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Login {
authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY
}
}
}
}
@Test
fun `OAuth2 login when authentication matcher configured then custom matcher used`() {
this.spring.register(AuthenticationMatcherConfig::class.java, ClientConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
verify(AuthenticationMatcherConfig.AUTHENTICATION_MATCHER).matches(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationMatcherConfig {
companion object {
var AUTHENTICATION_MATCHER: ServerWebExchangeMatcher = mock(ServerWebExchangeMatcher::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Login {
authenticationMatcher = AUTHENTICATION_MATCHER
}
}
}
}
@Test
fun `OAuth2 login when authentication converter configured then custom converter used`() {
this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire()
this.client.get()
.uri("/login/oauth2/code/google")
.exchange()
verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationConverterConfig {
companion object {
var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
oauth2Login {
authenticationConverter = AUTHENTICATION_CONVERTER
}
}
}
}
@Configuration
open class ClientConfig {
@Bean
open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository {
return InMemoryReactiveClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE
.getBuilder("google").clientId("clientId").clientSecret("clientSecret")
.build()
)
}
}
}

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.server.ServerWebExchange
import java.math.BigInteger
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAPublicKeySpec
/**
* Tests for [ServerOAuth2ResourceServerDsl]
*
* @author Eleftheria Stein
*/
class ServerOAuth2ResourceServerDslTests {
private val validJwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q"
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when custom access denied handler configured then custom handler used`() {
this.spring.register(AccessDeniedHandlerConfig::class.java).autowire()
this.client.get()
.uri("/")
.headers { it.setBearerAuth(validJwt) }
.exchange()
.expectStatus().isSeeOther
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AccessDeniedHandlerConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, hasAuthority("ADMIN"))
}
oauth2ResourceServer {
accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER)
jwt {
publicKey = publicKey()
}
}
}
}
}
@Test
fun `request when custom entry point configured then custom entry point used`() {
this.spring.register(AuthenticationEntryPointConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectStatus().isSeeOther
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationEntryPointConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
authenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.SEE_OTHER)
jwt {
publicKey = publicKey()
}
}
}
}
}
@Test
fun `request when custom bearer token converter configured then custom converter used`() {
this.spring.register(BearerTokenConverterConfig::class.java).autowire()
this.client.get()
.uri("/")
.headers { it.setBearerAuth(validJwt) }
.exchange()
verify(BearerTokenConverterConfig.CONVERTER).convert(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class BearerTokenConverterConfig {
companion object {
val CONVERTER: ServerBearerTokenAuthenticationConverter = mock(ServerBearerTokenAuthenticationConverter::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
bearerTokenConverter = CONVERTER
jwt {
publicKey = publicKey()
}
}
}
}
}
@Test
fun `request when custom authentication manager resolver configured then custom resolver used`() {
this.spring.register(AuthenticationManagerResolverConfig::class.java).autowire()
this.client.get()
.uri("/")
.headers { it.setBearerAuth(validJwt) }
.exchange()
verify(AuthenticationManagerResolverConfig.RESOLVER).resolve(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationManagerResolverConfig {
companion object {
val RESOLVER: ReactiveAuthenticationManagerResolver<ServerWebExchange> =
mock(ReactiveAuthenticationManagerResolver::class.java) as ReactiveAuthenticationManagerResolver<ServerWebExchange>
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = RESOLVER
}
}
}
}
companion object {
private fun publicKey(): RSAPublicKey {
val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"
val exponent = "65537"
val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent))
val factory = KeyFactory.getInstance("RSA")
return factory.generatePublic(spec) as RSAPublicKey
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.savedrequest.ServerRequestCache
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
/**
* Tests for [ServerRequestCacheDsl]
*
* @author Eleftheria Stein
*/
class ServerRequestCacheDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `GET when request cache enabled then redirected to cached page`() {
this.spring.register(RequestCacheConfig::class.java, UserDetailsConfig::class.java).autowire()
`when`(RequestCacheConfig.REQUEST_CACHE.removeMatchingRequest(any())).thenReturn(Mono.empty())
this.client.get()
.uri("/test")
.exchange()
verify(RequestCacheConfig.REQUEST_CACHE).saveRequest(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class RequestCacheConfig {
companion object {
var REQUEST_CACHE: ServerRequestCache = Mockito.mock(ServerRequestCache::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
requestCache {
requestCache = REQUEST_CACHE
}
}
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.http.client.reactive.ClientHttpConnector
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
import org.springframework.http.server.reactive.SslInfo
import org.springframework.lang.Nullable
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
import org.springframework.test.web.reactive.server.MockServerConfigurer
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.WebTestClientConfigurer
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.ServerWebExchangeDecorator
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import reactor.core.publisher.Mono
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
/**
* Tests for [ServerX509Dsl]
*
* @author Eleftheria Stein
*/
class ServerX509DslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `x509 when configured with defaults then user authenticated with expected username`() {
this.spring
.register(X509DefaultConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java)
.autowire()
val certificate = loadCert<X509Certificate>("rod.cer")
this.client
.mutateWith(mockX509(certificate))
.get()
.uri("/username")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("rod")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class X509DefaultConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
x509 { }
}
}
}
@Test
fun `x509 when principal extractor customized then custom principal extractor used`() {
this.spring
.register(PrincipalExtractorConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java)
.autowire()
val certificate = loadCert<X509Certificate>("rodatexampledotcom.cer")
this.client
.mutateWith(mockX509(certificate))
.get()
.uri("/username")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("rod")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PrincipalExtractorConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val customPrincipalExtractor = SubjectDnX509PrincipalExtractor()
customPrincipalExtractor.setSubjectDnRegex("CN=(.*?)@example.com(?:,|$)")
return http {
x509 {
principalExtractor = customPrincipalExtractor
}
}
}
}
@Test
fun `x509 when authentication manager customized then custom authentication manager used`() {
this.spring
.register(AuthenticationManagerConfig::class.java, UsernameController::class.java)
.autowire()
val certificate = loadCert<X509Certificate>("rod.cer")
this.client
.mutateWith(mockX509(certificate))
.get()
.uri("/username")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("rod")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationManagerConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
x509 {
authenticationManager = ReactivePreAuthenticatedAuthenticationManager(userDetailsService())
}
}
}
fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("rod")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
@RestController
class UsernameController {
@GetMapping("/username")
fun principal(@AuthenticationPrincipal user: User?): String {
return user!!.username
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("rod")
.password("password")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
private fun mockX509(certificate: X509Certificate): X509Mutator {
return X509Mutator(certificate)
}
private class X509Mutator internal constructor(private var certificate: X509Certificate) : WebTestClientConfigurer, MockServerConfigurer {
override fun afterConfigurerAdded(builder: WebTestClient.Builder,
@Nullable httpHandlerBuilder: WebHttpHandlerBuilder?,
@Nullable connector: ClientHttpConnector?) {
val filter = SetSslInfoWebFilter(certificate)
httpHandlerBuilder!!.filters { filters: MutableList<WebFilter?> -> filters.add(0, filter) }
}
}
private class SetSslInfoWebFilter(var certificate: X509Certificate) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(decorate(exchange))
}
private fun decorate(exchange: ServerWebExchange): ServerWebExchange {
val decorated: ServerHttpRequestDecorator = object : ServerHttpRequestDecorator(exchange.request) {
override fun getSslInfo(): SslInfo {
val sslInfo = mock(SslInfo::class.java)
`when`(sslInfo.sessionId).thenReturn("sessionId")
`when`(sslInfo.peerCertificates).thenReturn(arrayOf(certificate))
return sslInfo
}
}
return object : ServerWebExchangeDecorator(exchange) {
override fun getRequest(): org.springframework.http.server.reactive.ServerHttpRequest {
return decorated
}
}
}
}
private fun <T : Certificate> loadCert(location: String): T {
ClassPathResource(location).inputStream.use { inputStream ->
val certFactory = CertificateFactory.getInstance("X.509")
return certFactory.generateCertificate(inputStream) as T
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerCacheControlDsl]
*
* @author Eleftheria Stein
*/
class ServerCacheControlDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when cache control configured then cache headers in response`() {
this.spring.register(CacheControlConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CacheControlConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
cache { }
}
}
}
}
@Test
fun `request when cache control disabled then no cache headers in response`() {
this.spring.register(CacheControlDisabledConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL)
.expectHeader().doesNotExist(HttpHeaders.EXPIRES)
.expectHeader().doesNotExist(HttpHeaders.PRAGMA)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CacheControlDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
cache {
disable()
}
}
}
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerContentSecurityPolicyDsl]
*
* @author Eleftheria Stein
*/
class ServerContentSecurityPolicyDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when content security policy configured then content security policy header in response`() {
this.spring.register(ContentSecurityPolicyConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ContentSecurityPolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
contentSecurityPolicy { }
}
}
}
}
@Test
fun `request when custom policy directives then custom policy directive in response header`() {
this.spring.register(CustomPolicyDirectivesConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'; script-src trustedscripts.example.com")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomPolicyDirectivesConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
contentSecurityPolicy {
policyDirectives = "default-src 'self'; script-src trustedscripts.example.com"
}
}
}
}
}
@Test
fun `request when report only configured then content security policy report only header in response`() {
this.spring.register(ReportOnlyConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY, "default-src 'self'")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ReportOnlyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
contentSecurityPolicy {
reportOnly = true
}
}
}
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerContentTypeOptionsDsl]
*
* @author Eleftheria Stein
*/
class ServerContentTypeOptionsDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when content type options configured then header in response`() {
this.spring.register(ContentTypeOptionsConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ContentTypeOptionsConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
contentTypeOptions { }
}
}
}
}
@Test
fun `request when content type options disabled then no content type options header in response`() {
this.spring.register(ContentTypeOptionsDisabledConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ContentTypeOptionsDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
contentTypeOptions {
disable()
}
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerFrameOptionsDsl]
*
* @author Eleftheria Stein
*/
class ServerFrameOptionsDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when frame options configured then header in response`() {
this.spring.register(FrameOptionsConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class FrameOptionsConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
frameOptions { }
}
}
}
}
@Test
fun `request when frame options disabled then no frame options header in response`() {
this.spring.register(FrameOptionsDisabledConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class FrameOptionsDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
frameOptions {
disable()
}
}
}
}
}
@Test
fun `request when frame options mode set then frame options response header has mode value`() {
this.spring.register(CustomModeConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN.name)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomModeConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
frameOptions {
mode = XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN
}
}
}
}
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import java.time.Duration
/**
* Tests for [ServerReferrerPolicyDsl]
*
* @author Eleftheria Stein
*/
class ServerHttpStrictTransportSecurityDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when hsts configured then hsts header in response`() {
this.spring.register(HstsConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HstsConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
hsts { }
}
}
}
}
@Test
fun `request when hsts disabled then no hsts header in response`() {
this.spring.register(HstsDisabledConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class HstsDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
hsts {
disable()
}
}
}
}
}
@Test
fun `request when max age set then max age in response header`() {
this.spring.register(MaxAgeConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=1 ; includeSubDomains")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class MaxAgeConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
hsts {
maxAge = Duration.ofSeconds(1)
}
}
}
}
}
@Test
fun `request when includeSubdomains false then includeSubdomains not in response header`() {
this.spring.register(IncludeSubdomainsConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class IncludeSubdomainsConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
hsts {
includeSubdomains = false
}
}
}
}
}
@Test
fun `request when preload true then preload included in response header`() {
this.spring.register(PreloadConfig::class.java).autowire()
this.client.get()
.uri("https://example.com")
.exchange()
.expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains ; preload")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PreloadConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
hsts {
preload = true
}
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerReferrerPolicyDsl]
*
* @author Eleftheria Stein
*/
class ServerReferrerPolicyDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when referrer policy configured then referrer policy header in response`() {
this.spring.register(ReferrerPolicyConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER.policy)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class ReferrerPolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
referrerPolicy { }
}
}
}
}
@Test
fun `request when custom policy configured then custom policy in response header`() {
this.spring.register(CustomPolicyConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN.policy)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomPolicyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
referrerPolicy {
policy = ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN
}
}
}
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.headers
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Tests for [ServerXssProtectionDsl]
*
* @author Eleftheria Stein
*/
class ServerXssProtectionDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when xss protection configured then xss header in response`() {
this.spring.register(XssConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class XssConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
xssProtection { }
}
}
}
}
@Test
fun `request when xss protection disabled then no xss header in response`() {
this.spring.register(XssDisabledConfig::class.java).autowire()
this.client.get()
.uri("/")
.exchange()
.expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)
}
@EnableWebFluxSecurity
@EnableWebFlux
open class XssDisabledConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
headers {
xssProtection {
disable()
}
}
}
}
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.oauth2.resourceserver
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.core.convert.converter.Converter
import org.springframework.http.HttpHeaders
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAPublicKeySpec
import javax.annotation.PreDestroy
/**
* Tests for [ServerJwtDsl]
*
* @author Eleftheria Stein
*/
class ServerJwtDslTests {
private val expired = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzUwMzc4OTd9.jqZDDjfc2eysX44lHXEIr9XFd2S8vjIZHCccZU-dRWMRJNsQ1QN5VNnJGklqJBXJR4qgla6cmVqPOLkUHDb0sL0nxM5XuzQaG5ZzKP81RV88shFyAiT0fD-6nl1k-Fai-Fu-VkzSpNXgeONoTxDaYhdB-yxmgrgsApgmbOTE_9AcMk-FQDXQ-pL9kynccFGV0lZx4CA7cyknKN7KBxUilfIycvXODwgKCjj_1WddLTCNGYogJJSg__7NoxzqbyWd3udbHVjqYq7GsMMrGB4_2kBD4CkghOSNcRHbT_DIXowxfAVT7PAg7Q0E5ruZsr2zPZacEUDhJ6-wbvlA0FAOUg"
private val messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q"
private val jwkSet = "{\n" +
" \"keys\":[\n" +
" {\n" +
" \"kty\":\"RSA\",\n" +
" \"e\":\"AQAB\",\n" +
" \"use\":\"sig\",\n" +
" \"kid\":\"one\",\n" +
" \"n\":\"0IUjrPZDz-3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh-64VhYUa-vz17zYCmuB8fFj4XHE3MLkWIG-AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn-85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp-Cln6JROJcZUIRJ-QvS6QONWeS2407uQmS-i-lybsqaH0ldYC7NBEBA5inPQ\"\n" +
" }\n" +
" ]\n" +
"}\n"
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `request when JWT configured with public key and valid token then responds with ok`() {
this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire()
this.client.get()
.uri("/")
.headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) }
.exchange()
.expectStatus().isOk
}
@Test
fun `request when JWT configured with public key and expired token then responds with unauthorized`() {
this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire()
this.client.get()
.uri("/")
.headers { headers: HttpHeaders -> headers.setBearerAuth(expired) }
.exchange()
.expectStatus().isUnauthorized
}
@EnableWebFluxSecurity
@EnableWebFlux
open class PublicKeyConfig {
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
publicKey = publicKey()
}
}
}
}
}
@Test
fun `jwt when using custom JWT decoded then custom decoded used`() {
this.spring.register(CustomDecoderConfig::class.java).autowire()
this.client.get()
.uri("/")
.headers { headers: HttpHeaders -> headers.setBearerAuth("token") }
.exchange()
verify(CustomDecoderConfig.JWT_DECODER).decode("token")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomDecoderConfig {
companion object {
var JWT_DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = JWT_DECODER
}
}
}
}
}
@Test
fun `jwt when using custom JWK Set URI then custom URI used`() {
this.spring.register(CustomJwkSetUriConfig::class.java).autowire()
CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
this.client.get()
.uri("/")
.headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) }
.exchange()
val recordedRequest = CustomJwkSetUriConfig.MOCK_WEB_SERVER.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/.well-known/jwks.json")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomJwkSetUriConfig {
companion object {
var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = mockWebServer().url("/.well-known/jwks.json").toString()
}
}
}
}
@Bean
open fun mockWebServer(): MockWebServer {
return MOCK_WEB_SERVER
}
@PreDestroy
open fun shutdown() {
MOCK_WEB_SERVER.shutdown()
}
}
@Test
fun `opaque token when custom JWT authentication converter then converter used`() {
this.spring.register(CustomJwtAuthenticationConverterConfig::class.java).autowire()
`when`(CustomJwtAuthenticationConverterConfig.DECODER.decode(anyString())).thenReturn(
Mono.just(Jwt.withTokenValue("token")
.header("alg", "none")
.claim(IdTokenClaimNames.SUB, "user")
.build()))
`when`(CustomJwtAuthenticationConverterConfig.CONVERTER.convert(any()))
.thenReturn(Mono.just(TestingAuthenticationToken("test", "this", "ROLE")))
this.client.get()
.uri("/")
.headers { headers: HttpHeaders -> headers.setBearerAuth("token") }
.exchange()
verify(CustomJwtAuthenticationConverterConfig.CONVERTER).convert(any())
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomJwtAuthenticationConverterConfig {
companion object {
var CONVERTER: Converter<Jwt, out Mono<AbstractAuthenticationToken>> = mock(Converter::class.java) as Converter<Jwt, out Mono<AbstractAuthenticationToken>>
var DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java)
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CONVERTER
}
}
}
}
@Bean
open fun jwtDecoder(): ReactiveJwtDecoder {
return DECODER
}
}
@RestController
internal class BaseController {
@GetMapping
fun index() {
}
}
companion object {
private fun publicKey(): RSAPublicKey {
val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"
val exponent = "65537"
val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent))
val factory = KeyFactory.getInstance("RSA")
return factory.generatePublic(spec) as RSAPublicKey
}
}
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server.oauth2.resourceserver
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestRule
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import javax.annotation.PreDestroy
/**
* Tests for [ServerOpaqueTokenDsl]
*
* @author Eleftheria Stein
*/
class ServerOpaqueTokenDslTests {
@Rule
@JvmField
val spring = SpringTestRule()
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `opaque token when using defaults then uses introspector bean`() {
this.spring.register(IntrospectorBeanConfig::class.java).autowire()
IntrospectorBeanConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
this.client.get()
.uri("/")
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.exchange()
val recordedRequest = IntrospectorBeanConfig.MOCK_WEB_SERVER.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/introspect")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class IntrospectorBeanConfig {
companion object {
var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
}
@Bean
open fun mockWebServer(): MockWebServer {
return MOCK_WEB_SERVER
}
@PreDestroy
open fun shutdown() {
MOCK_WEB_SERVER.shutdown()
}
@Bean
open fun tokenIntrospectionClient(): ReactiveOpaqueTokenIntrospector {
return NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspect").toString(), "client", "secret")
}
}
@Test
fun `opaque token when using custom introspector then introspector used`() {
this.spring.register(CustomIntrospectorConfig::class.java).autowire()
CustomIntrospectorConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
this.client.get()
.uri("/")
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.exchange()
val recordedRequest = CustomIntrospectorConfig.MOCK_WEB_SERVER.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/introspector")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomIntrospectorConfig {
companion object {
var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspector = NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspector").toString(), "client", "secret")
}
}
}
}
@Bean
open fun mockWebServer(): MockWebServer {
return MOCK_WEB_SERVER
}
@PreDestroy
open fun shutdown() {
MOCK_WEB_SERVER.shutdown()
}
}
@Test
fun `opaque token when using custom introspection URI and credentials then custom used`() {
this.spring.register(CustomIntrospectionUriAndCredentialsConfig::class.java).autowire()
CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.enqueue(MockResponse())
this.client.get()
.uri("/")
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.exchange()
val recordedRequest = CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/introspection-uri")
}
@EnableWebFluxSecurity
@EnableWebFlux
open class CustomIntrospectionUriAndCredentialsConfig {
companion object {
var MOCK_WEB_SERVER: MockWebServer = MockWebServer()
}
@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspectionUri = mockWebServer().url("/introspection-uri").toString()
introspectionClientCredentials("client", "secret")
}
}
}
}
@Bean
open fun mockWebServer(): MockWebServer {
return MOCK_WEB_SERVER
}
@PreDestroy
open fun shutdown() {
MOCK_WEB_SERVER.shutdown()
}
}
}