Skip to content

Commit 5c2d857

Browse files
authored
Merge pull request #48 from gooddata/LX-341
feat: Add support for JIT defined via Organization setting
2 parents a8b6df2 + f7565f9 commit 5c2d857

10 files changed

+287
-35
lines changed

gooddata-server-oauth2-autoconfigure/src/main/kotlin/AuthenticationStoreClient.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ interface AuthenticationStoreClient {
4040
*/
4141
fun getOrganizationByHostname(hostname: String): Mono<Organization>
4242

43+
/**
44+
* Retrieves [JitProvisioningSetting] that corresponds to provided `organizationId`.
45+
*
46+
* Returns `null` in case no [JitProvisioningSetting] can be found.
47+
*
48+
* @param organizationId ID of the organization
49+
* @return found `JitProvisioningSetting` or `null` in case no [JitProvisioningSetting] is found
50+
*/
51+
fun getJitProvisioningSetting(organizationId: String): Mono<JitProvisioningSetting>
52+
4353
/**
4454
* Retrieves [User] that corresponds to provided `organizationId` and `authenticationId` retrieved from
4555
* OIDC ID token.
@@ -213,3 +223,21 @@ data class User(
213223
var email: String? = null,
214224
var userGroups: List<String>? = null,
215225
)
226+
227+
/**
228+
* Represents JIT provisioning setting stored in the persistent storage.
229+
*
230+
* @property enabled the switch to enable/disable the JIT provisioning
231+
* @property userGroupsScopeEnabled the switch to enable/disable the user groups scope in authentication flow
232+
* @property userGroupsScopeName the name of the OIDC scope used for user groups provisioning
233+
* @property userGroupsDefaults the list of default user groups to be used if user groups are not to be shared via OIDC
234+
* authentication flow
235+
*
236+
* @see AuthenticationStoreClient
237+
*/
238+
data class JitProvisioningSetting(
239+
val enabled: Boolean,
240+
val userGroupsScopeEnabled: Boolean = false,
241+
val userGroupsScopeName: String? = null,
242+
val userGroupsDefaults: List<String>? = null
243+
)

gooddata-server-oauth2-autoconfigure/src/main/kotlin/AuthenticationUtils.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
@file:Suppress("TooManyFunctions")
17+
1718
package com.gooddata.oauth2.server
1819

1920
import com.gooddata.oauth2.server.OAuthConstants.GD_USER_GROUPS_SCOPE
@@ -34,8 +35,10 @@ import com.nimbusds.oauth2.sdk.ParseException
3435
import com.nimbusds.oauth2.sdk.Scope
3536
import com.nimbusds.openid.connect.sdk.OIDCScopeValue
3637
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
38+
import java.net.URI
3739
import java.security.MessageDigest
3840
import java.time.Instant
41+
import java.util.Collections
3942
import net.minidev.json.JSONObject
4043
import org.springframework.core.ParameterizedTypeReference
4144
import org.springframework.core.convert.ConversionService
@@ -60,8 +63,6 @@ import org.springframework.web.client.RestTemplate
6063
import org.springframework.web.server.ResponseStatusException
6164
import org.springframework.web.util.UriComponentsBuilder
6265
import reactor.core.publisher.Mono
63-
import java.net.URI
64-
import java.util.Collections
6566

6667
/**
6768
* Constants for OAuth type authentication which are not directly available in the Spring Security.
@@ -111,12 +112,13 @@ private val typeReference: ParameterizedTypeReference<Map<String, Any>> = object
111112
fun buildClientRegistration(
112113
registrationId: String,
113114
organization: Organization,
115+
jitProvisioningSetting: JitProvisioningSetting,
114116
properties: HostBasedClientRegistrationRepositoryProperties,
115117
clientRegistrationCache: ClientRegistrationCache,
116118
): ClientRegistration {
117119
val issuerLocation = organization.oauthIssuerLocation
118120
// fallback to DEX client registration if issuer location is not defined
119-
?: return dexClientRegistration(registrationId, properties, organization)
121+
?: return dexClientRegistration(registrationId, properties, organization, jitProvisioningSetting)
120122

121123
// fetch the cached settings obtained from the well-known endpoint for the particular issuerLocation
122124
// this saves HTTP requests to well known endpoints everytime the API call is authorized
@@ -136,7 +138,7 @@ fun buildClientRegistration(
136138
return ClientRegistration.withClientRegistration(cachedRegistration)
137139
.registrationId(registrationId)
138140
.withRedirectUri(organization.oauthIssuerId)
139-
.buildWithIssuerConfig(organization)
141+
.buildWithIssuerConfig(organization, jitProvisioningSetting)
140142
}
141143

142144
/**
@@ -165,11 +167,12 @@ private fun ClientRegistration.Builder.buildWithDummyValues(): ClientRegistratio
165167
private fun dexClientRegistration(
166168
registrationId: String,
167169
properties: HostBasedClientRegistrationRepositoryProperties,
168-
organization: Organization
170+
organization: Organization,
171+
jitProvisioningSetting: JitProvisioningSetting
169172
): ClientRegistration = ClientRegistration
170173
.withRegistrationId(registrationId)
171174
.withDexConfig(properties)
172-
.buildWithIssuerConfig(organization)
175+
.buildWithIssuerConfig(organization, jitProvisioningSetting)
173176

174177
/**
175178
* Handles client registration for Azure B2C by validating issuer metadata and building the registration.
@@ -318,6 +321,7 @@ private fun handleRuntimeException(ex: RuntimeException, issuerLocation: String)
318321
HttpStatus.UNAUTHORIZED,
319322
"Authorization failed for given issuer \"$issuerLocation\". ${ex.message}"
320323
)
324+
321325
else -> throw ex
322326
}
323327
}
@@ -426,6 +430,7 @@ private fun ClientRegistration.Builder.withRedirectUri(oauthIssuerId: String?) =
426430
*/
427431
internal fun ClientRegistration.Builder.buildWithIssuerConfig(
428432
organization: Organization,
433+
jitProvisioningSetting: JitProvisioningSetting
429434
): ClientRegistration {
430435
if (organization.oauthClientId == null || organization.oauthClientSecret == null) {
431436
throw ResponseStatusException(
@@ -439,7 +444,7 @@ internal fun ClientRegistration.Builder.buildWithIssuerConfig(
439444
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
440445
.userNameAttributeName("name")
441446
val supportedScopes = withIssuerConfigBuilder.build().resolveSupportedScopes()
442-
return withIssuerConfigBuilder.withScopes(supportedScopes, organization).build()
447+
return withIssuerConfigBuilder.withScopes(supportedScopes, organization, jitProvisioningSetting).build()
443448
}
444449

445450
private fun ClientRegistration.resolveSupportedScopes() =
@@ -449,18 +454,25 @@ private fun ClientRegistration.resolveSupportedScopes() =
449454

450455
private fun ClientRegistration.Builder.withScopes(
451456
supportedScopes: Scope?,
452-
organization: Organization
453-
457+
organization: Organization,
458+
jitProvisioningSetting: JitProvisioningSetting,
454459
): ClientRegistration.Builder {
455460
// in the future, we could check mandatory scopes against the supported ones
456461
val mandatoryScopes = listOf(OIDCScopeValue.OPENID, OIDCScopeValue.PROFILE).map(Scope.Value::getValue)
457462
val userGroupsScope = if (organization.jitEnabled == true) {
458463
listOf(OIDCScopeValue.EMAIL.value, GD_USER_GROUPS_SCOPE)
464+
} else if (jitProvisioningSetting.enabled) {
465+
if (jitProvisioningSetting.userGroupsScopeEnabled) {
466+
listOf(OIDCScopeValue.EMAIL.value, jitProvisioningSetting.userGroupsScopeName ?: GD_USER_GROUPS_SCOPE)
467+
} else {
468+
listOf(OIDCScopeValue.EMAIL.value)
469+
}
459470
} else {
460471
listOf()
461472
}
462473
val azureB2CScope = if (organization.oauthIssuerLocation != null &&
463-
organization.oauthIssuerLocation.toUri().isAzureB2C()) {
474+
organization.oauthIssuerLocation.toUri().isAzureB2C()
475+
) {
464476
listOf(organization.oauthClientId)
465477
} else {
466478
listOf()

gooddata-server-oauth2-autoconfigure/src/main/kotlin/HostBasedReactiveClientRegistrationRepository.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,21 @@ import reactor.core.publisher.Mono
2626
class HostBasedReactiveClientRegistrationRepository(
2727
private val properties: HostBasedClientRegistrationRepositoryProperties,
2828
private val clientRegistrationCache: ClientRegistrationCache,
29+
private val client: AuthenticationStoreClient
2930
) : ReactiveClientRegistrationRepository {
3031

3132
override fun findByRegistrationId(registrationId: String): Mono<ClientRegistration> =
32-
getOrganizationFromContext().map {
33-
buildClientRegistration(
34-
registrationId = registrationId,
35-
organization = it,
36-
properties = properties,
37-
clientRegistrationCache = clientRegistrationCache,
38-
)
33+
getOrganizationFromContext().flatMap { organization ->
34+
client.getJitProvisioningSetting(organization.id)
35+
.defaultIfEmpty(JitProvisioningSetting(enabled = false))
36+
.map { jitProvisioningSetting ->
37+
buildClientRegistration(
38+
registrationId = registrationId,
39+
organization = organization,
40+
jitProvisioningSetting = jitProvisioningSetting,
41+
properties = properties,
42+
clientRegistrationCache = clientRegistrationCache,
43+
)
44+
}
3945
}
4046
}

gooddata-server-oauth2-autoconfigure/src/main/kotlin/JitProvisioningAuthenticationSuccessHandler.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,30 @@ class JitProvisioningAuthenticationSuccessHandler(
5252
if (organization.jitEnabled == true) {
5353
provisionUser(authenticationToken, organization)
5454
} else {
55-
logMessage("JIT provisioning disabled, skipping", "finished", "")
56-
Mono.empty()
55+
client.getJitProvisioningSetting(organization.id).flatMap {
56+
if (it.enabled) {
57+
provisionUser(authenticationToken, organization, it.userGroupsDefaults)
58+
} else {
59+
logMessage("JIT provisioning disabled, skipping", "finished", "")
60+
Mono.empty()
61+
}
62+
}
5763
}
5864
}
5965
}
6066

6167
private fun provisionUser(
6268
authenticationToken: OAuth2AuthenticationToken,
63-
organization: Organization
69+
organization: Organization,
70+
userGroups: List<String>? = null
6471
): Mono<User> {
6572
checkMandatoryClaims(authenticationToken, organization.id)
6673
logMessage("Initiating JIT provisioning", "started", organization.id)
6774
val subClaim = authenticationToken.getClaim(organization.oauthSubjectIdClaim)
6875
val firstnameClaim = authenticationToken.getClaim(GIVEN_NAME)
6976
val lastnameClaim = authenticationToken.getClaim(FAMILY_NAME)
7077
val emailClaim = authenticationToken.getClaim(EMAIL)
71-
val userGroupsClaim = authenticationToken.getClaimList(GD_USER_GROUPS)
78+
val userGroupsClaim = authenticationToken.getClaimList(GD_USER_GROUPS) ?: userGroups
7279

7380
return client.getUserByAuthenticationId(organization.id, subClaim)
7481
.flatMap { user ->

gooddata-server-oauth2-autoconfigure/src/main/kotlin/ServerOAuth2AutoConfiguration.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,13 @@ class ServerOAuth2AutoConfiguration {
115115
client: ObjectProvider<AuthenticationStoreClient>,
116116
properties: HostBasedClientRegistrationRepositoryProperties,
117117
clientRegistrationCache: ClientRegistrationCache,
118+
authenticationStoreClient: ObjectProvider<AuthenticationStoreClient>
118119
): ReactiveClientRegistrationRepository =
119-
HostBasedReactiveClientRegistrationRepository(properties, clientRegistrationCache)
120+
HostBasedReactiveClientRegistrationRepository(
121+
properties,
122+
clientRegistrationCache,
123+
authenticationStoreClient.`object`
124+
)
120125

121126
@ConditionalOnMissingBean(ClientRegistrationCache::class)
122127
@Bean

0 commit comments

Comments
 (0)