Idiomatic Kotlin DSL for server HTTP security
Issue: gh-5558
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user