Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.ser
import org.springframework.security.config.annotation.web.oauth2.resourceserver.DPoPDsl
import org.springframework.security.config.annotation.web.oauth2.resourceserver.JwtDsl
import org.springframework.security.config.annotation.web.oauth2.resourceserver.OpaqueTokenDsl
import org.springframework.security.config.annotation.web.oauth2.resourceserver.ProtectedResourceMetadataDsl
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.access.AccessDeniedHandler
Expand All @@ -33,6 +34,7 @@ import org.springframework.security.web.access.AccessDeniedHandler
*
* @author Eleftheria Stein
* @author Max Batischev
* @author Andrey Litvitski
* @since 5.3
* @property accessDeniedHandler the [AccessDeniedHandler] to use for requests authenticating
* with <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s.
Expand All @@ -51,6 +53,7 @@ class OAuth2ResourceServerDsl {
private var jwt: ((OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer) -> Unit)? = null
private var opaqueToken: ((OAuth2ResourceServerConfigurer<HttpSecurity>.OpaqueTokenConfigurer) -> Unit)? = null
private var dPoP: ((OAuth2ResourceServerConfigurer<HttpSecurity>.DPoPConfigurer) -> Unit)? = null
private var protectedResourceMetadata: ((OAuth2ResourceServerConfigurer.ProtectedResourceMetadataConfigurer) -> Unit)? = null

/**
* Enables JWT-encoded bearer token support.
Expand Down Expand Up @@ -143,6 +146,40 @@ class OAuth2ResourceServerDsl {
this.dPoP = DPoPDsl().apply(dPoPConfig).get()
}

/**
* Enables OAuth 2.0 Protected Resource Metadata support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2ResourceServer {
* jwt { }
* protectedResourceMetadata {
* protectedResourceMetadataCustomizer { }
* }
* }
* }
* return http.build()
* }
* }
* ```
*
* @param protectedResourceMetadataConfig custom configurations to configure OAuth 2.0
* Protected Resource Metadata support
* @see [ProtectedResourceMetadataDsl]
* @since 7.1
*/
fun protectedResourceMetadata(protectedResourceMetadataConfig: ProtectedResourceMetadataDsl.() -> Unit) {
this.protectedResourceMetadata = ProtectedResourceMetadataDsl().apply(protectedResourceMetadataConfig).get()
}

internal fun get(): (OAuth2ResourceServerConfigurer<HttpSecurity>) -> Unit {
return { oauth2ResourceServer ->
accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) }
Expand All @@ -152,6 +189,7 @@ class OAuth2ResourceServerDsl {
jwt?.also { oauth2ResourceServer.jwt(jwt) }
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
dPoP?.also { oauth2ResourceServer.dPoP(dPoP) }
protectedResourceMetadata?.also { oauth2ResourceServer.protectedResourceMetadata(protectedResourceMetadata) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2004-present 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.annotation.web.oauth2.resourceserver

import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadata

/**
* A Kotlin DSL to configure OAuth 2.0 Protected Resource Metadata support using
* idiomatic Kotlin code.
*
* @author Andrey Litvitski
* @since 7.1
*/
@OAuth2ResourceServerSecurityMarker
class ProtectedResourceMetadataDsl {

private var protectedResourceMetadataCustomizer: ((OAuth2ProtectedResourceMetadata.Builder) -> Unit)? = null

/**
* Sets the customizer providing access to the protected resource metadata builder.
*
* @param protectedResourceMetadataCustomizer the customizer providing access to the protected
* resource metadata builder.
*/
fun protectedResourceMetadataCustomizer(
protectedResourceMetadataCustomizer: (OAuth2ProtectedResourceMetadata.Builder) -> Unit
) {
this.protectedResourceMetadataCustomizer = protectedResourceMetadataCustomizer
}

internal fun get(): (OAuth2ResourceServerConfigurer.ProtectedResourceMetadataConfigurer) -> Unit {
return { protectedResourceMetadata ->
protectedResourceMetadataCustomizer?.also { customizer ->
protectedResourceMetadata.protectedResourceMetadataCustomizer { builder ->
customizer(builder)
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright 2004-present 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.annotation.web.oauth2.resourceserver

import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.hasSize
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.oauth2.jose.TestKeys
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadataClaimNames
import org.springframework.security.web.SecurityFilterChain
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

/**
* Tests for [ProtectedResourceMetadataDsl].
*
* @author Andrey Litvitski
*/
class ProtectedResourceMetadataDslTests {

Comment on lines +42 to +44

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make this test logically fully equivalent to the existing OAuth2ProtectedResourceMetadataTests.java.

@JvmField
val spring = SpringTestContext(this)

@Autowired
lateinit var mockMvc: MockMvc

@Test
fun requestWhenProtectedResourceMetadataRequestThenReturnMetadataResponse() {
this.spring.register(ResourceServerConfiguration::class.java).autowire()
this.mockMvc.perform(
MockMvcRequestBuilders.get(RESOURCE + DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI)
)
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE).value(RESOURCE))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).isArray())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).value(hasSize<Any>(1)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).value(hasItem("header")))
.andExpect(
MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS)
.value(true)
)
.andReturn()
}

@Test
fun requestWhenProtectedResourceMetadataRequestIncludesResourcePathThenMetadataResponseHasResourcePath() {
this.spring.register(ResourceServerConfiguration::class.java).autowire()
val host = RESOURCE
var resourcePath = "/resource1"
var resource = host + resourcePath
this.mockMvc.perform(
MockMvcRequestBuilders.get(host + DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI + resourcePath)
)
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE).value(resource))
.andReturn()
resourcePath = "/path1/resource2"
resource = host + resourcePath
this.mockMvc.perform(
MockMvcRequestBuilders.get(host + DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI + resourcePath)
)
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE).value(resource))
.andReturn()
resourcePath = "/path1/path2/resource3"
resource = host + resourcePath
this.mockMvc.perform(
MockMvcRequestBuilders.get(host + DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI + resourcePath)
)
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE).value(resource))
.andReturn()
}

@Test
fun requestWhenProtectedResourceMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() {
this.spring.register(ResourceServerConfigurationWithMetadataCustomizer::class.java).autowire()
this.mockMvc.perform(
MockMvcRequestBuilders.get(RESOURCE + DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI)
)
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE).value(RESOURCE))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS).isArray())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS).value(hasSize<Any>(2)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS).value(hasItem(ISSUER_1)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS).value(hasItem(ISSUER_2)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED).isArray())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED).value(hasSize<Any>(2)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED).value(hasItem("scope1")))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED).value(hasItem("scope2")))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).isArray())
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).value(hasSize<Any>(1)))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED).value(hasItem("header")))
.andExpect(MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE_NAME).value("resourceName"))
.andExpect(
MockMvcResultMatchers.jsonPath(OAuth2ProtectedResourceMetadataClaimNames.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS)
.value(true)
)
.andReturn()
}

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
internal open class ResourceServerConfiguration {

@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}

@Bean
open fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build()
}

}

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
internal open class ResourceServerConfigurationWithMetadataCustomizer : ResourceServerConfiguration() {

@Bean
override fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
protectedResourceMetadata {
protectedResourceMetadataCustomizer { protectedResourceMetadata ->
protectedResourceMetadata.authorizationServer(ISSUER_1)
.authorizationServer(ISSUER_2)
.scope("scope1")
.scope("scope2")
.resourceName("resourceName")
}
}
}
}
return http.build()
}

}

companion object {

private const val DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI =
"/.well-known/oauth-protected-resource"

private const val RESOURCE = "https://resource.com:8443"

private const val ISSUER_1 = "https://provider1.com"

private const val ISSUER_2 = "https://provider2.com"

}

}
Loading