Skip to content

Commit b56d6c4

Browse files
committed
Fixes #28893: Support JWT in UserInfo endpoint response
1 parent a760e55 commit b56d6c4

2 files changed

Lines changed: 374 additions & 8 deletions

File tree

auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import bootstrap.liftweb.AuthenticationMethods
4242
import bootstrap.liftweb.RudderConfig
4343
import bootstrap.liftweb.RudderInMemoryUserDetailsService
4444
import bootstrap.liftweb.RudderProperties
45+
import cats.syntax.apply.*
4546
import com.github.benmanes.caffeine.cache.Caffeine
4647
import com.normation.errors.IOResult
4748
import com.normation.plugins.RudderPluginModule
@@ -72,6 +73,7 @@ import org.springframework.context.annotation.Bean
7273
import org.springframework.context.annotation.Configuration
7374
import org.springframework.core.ParameterizedTypeReference
7475
import org.springframework.core.convert.converter.Converter
76+
import org.springframework.http.MediaType
7577
import org.springframework.http.RequestEntity
7678
import org.springframework.http.ResponseEntity
7779
import org.springframework.security.authentication.AbstractAuthenticationToken
@@ -1050,7 +1052,7 @@ class RudderOidcUserService(
10501052
roleApiMapping: RoleApiMapping
10511053
) extends OidcUserService with RudderUserServerMapping[OidcUserRequest, OidcUser, RudderUserDetail & OidcUser] {
10521054
// we need to use our copy of DefaultOAuth2UserService to log/manage errors
1053-
super.setOauth2UserService(new RudderDefaultOAuth2UserService()): @unchecked
1055+
super.setOauth2UserService(new RudderDefaultOAuth2UserService(registrationRepository)): @unchecked
10541056

10551057
override val protocolId = RudderOidcUserService.PROTOCOL_ID
10561058
override val protocolName = "OIDC"
@@ -1077,7 +1079,7 @@ class RudderOAuth2UserService(
10771079
roleApiMapping: RoleApiMapping
10781080
) extends OAuth2UserService[OAuth2UserRequest, OAuth2User]
10791081
with RudderUserServerMapping[OAuth2UserRequest, OAuth2User, RudderUserDetail & OAuth2User] {
1080-
val defaultUserService = new RudderDefaultOAuth2UserService()
1082+
val defaultUserService = new RudderDefaultOAuth2UserService(registrationRepository)
10811083

10821084
override val protocolId = RudderOAuth2UserService.PROTOCOL_ID
10831085
override val protocolName = "OAuth2"
@@ -1130,7 +1132,8 @@ object BuildLogout {
11301132
}
11311133
}
11321134

1133-
class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService with DebugOAuth2Attributes {
1135+
class RudderDefaultOAuth2UserService(registrationRepository: RudderClientRegistrationRepository)
1136+
extends DefaultOAuth2UserService with DebugOAuth2Attributes {
11341137

11351138
/*
11361139
* this is a copy of parent method with more logs/error management
@@ -1147,7 +1150,9 @@ class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService with Debug
11471150
private val requestEntityConverter: Converter[OAuth2UserRequest, RequestEntity[?]] = new OAuth2UserRequestEntityConverter
11481151

11491152
private val restOperations: RestOperations = {
1150-
val restTemplate = new RestTemplate
1153+
val restTemplate = new RestTemplate()
1154+
// TODO: set message converters instead of intercepting exception ? https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html
1155+
// restTemplate.setMessageConverters(List(JwtConverter(registration.getProviderDetails.getJwkSetUri)).asJava)
11511156
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler)
11521157
restTemplate
11531158
}
@@ -1205,8 +1210,11 @@ class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService with Debug
12051210
userRequest: OAuth2UserRequest,
12061211
request: RequestEntity[?]
12071212
): ResponseEntity[java.util.Map[String, AnyRef]] = {
1213+
given jwtConfig: Option[JwtConfig] =
1214+
registrationRepository.registrations.get(userRequest.getClientRegistration.getRegistrationId).map(_.jwtConfig)
1215+
given ClientRegistration = userRequest.getClientRegistration
12081216
try {
1209-
return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE)
1217+
this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE)
12101218
} catch {
12111219
case ex: OAuth2AuthorizationException =>
12121220
var oauth2Error: OAuth2Error = ex.getError
@@ -1226,6 +1234,8 @@ class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService with Debug
12261234
null
12271235
)
12281236
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString, ex)
1237+
case JwtContentTypeException(jwtResponse) =>
1238+
jwtResponse
12291239
case ex: UnknownContentTypeException =>
12301240
val errorMessage: String =
12311241
"An error occurred while attempting to retrieve the UserInfo Resource from '" + userRequest.getClientRegistration.getProviderDetails.getUserInfoEndpoint.getUri + "': response contains invalid content type '" + ex.getContentType.toString + "'. " + "The UserInfo Response should return a JSON object (content type 'application/json') " + "that contains a collection of name and value pairs of the claims about the authenticated End-User. " + "Please ensure the UserInfo Uri in UserInfoEndpoint for Client Registration '" + userRequest.getClientRegistration.getRegistrationId + "' conforms to the UserInfo Endpoint, " + "as defined in OpenID Connect 1.0: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo'"
@@ -1565,3 +1575,23 @@ trait DebugOAuth2TokenAttributes extends DebugOAuth2Attributes {
15651575
def pivotAttributeRegistration: RegistrationWithPivotAttribute
15661576
def pivotAttribute: String = pivotAttributeRegistration.pivotAttributeName
15671577
}
1578+
1579+
// Special case for JWT interception upon content-type exception in getResponse : signed/encrypted response.
1580+
// Spring has no direct support for these, we need to use Nimbus for decrypting
1581+
object JwtContentTypeException {
1582+
1583+
private val APPLICATION_JWT: MediaType = new MediaType("application", "jwt")
1584+
1585+
def unapply(
1586+
ex: UnknownContentTypeException
1587+
)(using
1588+
jwtConfig: Option[JwtConfig],
1589+
clientRegistration: ClientRegistration
1590+
): Option[ResponseEntity[util.Map[String, AnyRef]]] = {
1591+
given JwtConfig = jwtConfig.getOrElse(JwtConfig.default(clientRegistration.getProviderDetails.getJwkSetUri))
1592+
Option(ex.getResponseHeaders).filter(_ => ex.getContentType.isCompatibleWith(APPLICATION_JWT)) *>
1593+
decodeJwt(ex.getResponseBodyAsString).toOption
1594+
.map(jwt => ResponseEntity.ok().body(jwt.getClaims))
1595+
}
1596+
1597+
}

0 commit comments

Comments
 (0)