Skip to content

Commit

Permalink
Merge pull request #12 from gooddata/LX-127
Browse files Browse the repository at this point in the history
LX-127: Add custom handler for empty username claim
  • Loading branch information
jeskepetr authored Feb 29, 2024
2 parents 1352387 + 958ed83 commit dc32432
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 GoodData Corporation
*
* 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 com.gooddata.oauth2.server

import mu.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Mono

/**
* Custom implementation of [DefaultReactiveOAuth2UserService] that introduces OAuth2User validation.
* If the validation is not successful the authentication should fail with status UNAUTHORIZED.
* Without this custom implementation, the authentication could fail with INTERNAL_SERVER_ERROR, when OAuth2User object
* is not valid.
*/
class CustomReactiveOAuth2UserService : DefaultReactiveOAuth2UserService() {
@Override
override fun loadUser(userRequest: OAuth2UserRequest): Mono<OAuth2User> {
return super.loadUser(userRequest)
.flatMap { user ->
OAuth2UserValidator().validateUser(userRequest, user)
}
}
}

/**
* OAuth2UserValidator that validates the user name attribute.
* The OAuth2User is considered invalid if user name attribute is not present or empty.
*/
class OAuth2UserValidator {
val logger = KotlinLogging.logger {}
fun validateUser(userRequest: OAuth2UserRequest, user: OAuth2User): Mono<OAuth2User> {
return Mono.just(user).handle { it, sink ->
val userNameAttrName = userRequest.clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName
val userNameAttribute = it.attributes[userNameAttrName] as String?
if (userNameAttribute.isNullOrEmpty()) {
logger.logInfo {
withMessage {
"Authentication failed! Required \"user name\" attribute name in UserInfoEndpoint " +
"contains invalid value for Client Registration. Client ID: " +
userRequest.clientRegistration.clientId
}
withAction("Process user Authentication")
withState("failure")
}
sink.error(
ResponseStatusException(
HttpStatus.UNAUTHORIZED,
"Authorization failed, \"user name\" attribute - $userNameAttrName contains invalid value!" +
" Please check your Client Registration settings."
)
)
return@handle
}
sink.next(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.gooddata.oauth2.server

import io.netty.channel.ChannelOption
import java.time.Duration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -29,7 +30,6 @@ import org.springframework.security.oauth2.client.endpoint.WebClientReactiveRefr
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter
Expand All @@ -40,7 +40,6 @@ import org.springframework.web.reactive.function.client.WebClient
import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider
import reactor.netty.resources.ConnectionProvider.DEFAULT_POOL_ACQUIRE_TIMEOUT
import java.time.Duration

private const val DEFAULT_MAX_CONNECTIONS = 500
private const val CUSTOM_CONNECTION_PROVIDER_NAME = "gdc-connection-provider"
Expand Down Expand Up @@ -102,7 +101,7 @@ class ReactiveCommunicationClientsConfiguration(private val httpProperties: Http

@Bean
fun oauth2UserService(webClient: WebClient): ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> =
DefaultReactiveOAuth2UserService().apply {
CustomReactiveOAuth2UserService().apply {
setWebClient(webClient)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2024 GoodData Corporation
*
* 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 com.gooddata.oauth2.server

import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.web.server.ResponseStatusException
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import strikt.assertions.isNotNull

class OAuth2UserValidatorTest {

private val userValidator = OAuth2UserValidator()
private val userRequest = mockk<OAuth2UserRequest> {
every { clientRegistration } returns mockk {
every { providerDetails } returns mockk {
every { userInfoEndpoint } returns mockk {
every { userNameAttributeName } returns "userName"
}
}
}
}

@Test
fun `should raise exception when user does not contain valid user name claim`() {
// given
val user = mockk<OAuth2User> {
every { attributes } returns mapOf("userName" to "")
}

// then
expectThrows<ResponseStatusException> {
userValidator.validateUser(userRequest, user).block()
}.and {
get { message }.isEqualTo(
"401 UNAUTHORIZED \"Authorization failed, \"user name\" attribute - userName contains invalid " +
"value! Please check your Client Registration settings.\""
)
}
}

@Test
fun `user should pass validation with valid userName attribute`() {
// given
val user = mockk<OAuth2User> {
every { attributes } returns mapOf("userName" to "Admin GoodData")
}

// then
expectThat(userValidator.validateUser(userRequest, user).block()) {
isNotNull().and { isEqualTo(user) }
}
}
}

0 comments on commit dc32432

Please sign in to comment.