Skip to content

Commit 009e4a4

Browse files
authored
Merge pull request #36 from gooddata/LX-573
feat: Apply custom auth attributes on authentication call
2 parents 9be0f7c + ededefa commit 009e4a4

File tree

4 files changed

+77
-19
lines changed

4 files changed

+77
-19
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ interface AuthenticationStoreClient {
177177
* @property oauthSubjectIdClaim name of the claim in ID token that will be used for finding the user in organization.
178178
* Defaults to `null` and it means that `sub` claim will be used.
179179
* @property jitEnabled the switch for enabling/disabling of the JIT provisioning
180+
* @property oauthCustomAuthAttributes map for additional oauth authentication attributes to be sent
181+
* in authentication request
180182
*
181183
* @see AuthenticationStoreClient
182184
*/
@@ -189,6 +191,7 @@ data class Organization(
189191
val oauthIssuerId: String? = null,
190192
val oauthSubjectIdClaim: String? = null,
191193
val jitEnabled: Boolean? = null,
194+
val oauthCustomAuthAttributes: Map<String, String>? = null,
192195
)
193196

194197
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package com.gooddata.oauth2.server
1717

18-
import com.gooddata.oauth2.server.oauth2.client.FederationAwareOauth2AuthorizationRequestResolver
18+
import com.gooddata.oauth2.server.oauth2.client.CustomAttrsAwareOauth2AuthorizationRequestResolver
1919
import java.util.Base64
2020
import org.springframework.beans.factory.ObjectProvider
2121
import org.springframework.beans.factory.annotation.Value
@@ -213,7 +213,7 @@ class ServerOAuth2AutoConfiguration {
213213
fun federationAwareAuthorizationRequestResolver(
214214
urlSafeStateAuthorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver,
215215
cookieService: ReactiveCookieService,
216-
) = FederationAwareOauth2AuthorizationRequestResolver(urlSafeStateAuthorizationRequestResolver, cookieService)
216+
) = CustomAttrsAwareOauth2AuthorizationRequestResolver(urlSafeStateAuthorizationRequestResolver, cookieService)
217217

218218
@Bean
219219
@Suppress("LongParameterList", "LongMethod")
Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@ package com.gooddata.oauth2.server.oauth2.client
1717

1818
import com.gooddata.oauth2.server.ReactiveCookieService
1919
import com.gooddata.oauth2.server.SPRING_EXTERNAL_IDP
20+
import com.gooddata.oauth2.server.getOrganizationFromContext
2021
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver
2122
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
2223
import org.springframework.web.server.ServerWebExchange
2324
import reactor.core.publisher.Mono
2425
import reactor.kotlin.core.publisher.switchIfEmpty
2526

2627
/**
27-
* The implementation of [ServerOAuth2AuthorizationRequestResolver] that is able to append external identity provider
28-
* (OIDC federation) info to authorization requests based on [SPRING_EXTERNAL_IDP] cookie in the [ServerWebExchange].
28+
* The implementation of [ServerOAuth2AuthorizationRequestResolver] that is able to append ad-hoc authentication attrs
29+
* to authorization requests.
30+
*
31+
* Firstly it can add an external identity provider (OIDC federation) info to authorization requests
32+
* based on [SPRING_EXTERNAL_IDP] cookie in the [ServerWebExchange].
2933
*
3034
* It wraps the default [ServerOAuth2AuthorizationRequestResolver] which is responsible for building of the original
3135
* authorization request with standard parameters.
@@ -34,31 +38,35 @@ import reactor.kotlin.core.publisher.switchIfEmpty
3438
*
3539
* The new query parameter is Cognito-specific for now, so this does not ensure support for other identity providers.
3640
*
41+
* Secondly, it can add additional authentication attributes if present in the organization definition.
42+
*
3743
* @param defaultResolver the default [ServerOAuth2AuthorizationRequestResolver] to be wrapped
3844
* @param cookieService the [ReactiveCookieService] to be used for cookie handling
3945
*/
40-
class FederationAwareOauth2AuthorizationRequestResolver(
46+
class CustomAttrsAwareOauth2AuthorizationRequestResolver(
4147
private val defaultResolver: ServerOAuth2AuthorizationRequestResolver,
4248
private val cookieService: ReactiveCookieService,
4349
) : ServerOAuth2AuthorizationRequestResolver {
4450
override fun resolve(exchange: ServerWebExchange?): Mono<OAuth2AuthorizationRequest> =
4551
defaultResolver.resolve(exchange).flatMap { authorizationRequest ->
46-
enhanceRequestByExternalIdentityParam(authorizationRequest, exchange)
52+
enhanceRequestByAdditionalParams(authorizationRequest, exchange)
4753
}
4854

4955
override fun resolve(
5056
exchange: ServerWebExchange?,
5157
clientRegistrationId: String?,
5258
): Mono<OAuth2AuthorizationRequest> =
5359
defaultResolver.resolve(exchange, clientRegistrationId).flatMap { authorizationRequest ->
54-
enhanceRequestByExternalIdentityParam(authorizationRequest, exchange)
60+
enhanceRequestByAdditionalParams(authorizationRequest, exchange)
5561
}
5662

5763
/**
58-
* Enhances the provided [authorizationRequest] with external identity provider info based
59-
* on the [SPRING_EXTERNAL_IDP] cookie existence. If the cookie is present, it is cleared.
64+
* Enhances the provided [authorizationRequest] with external additional authentication attributes.
65+
* Adds additional authentication attributes from the organization definition if they are present.
66+
* Adds identity provider info based on the [SPRING_EXTERNAL_IDP] cookie existence. If the cookie is present,
67+
* it is cleared.
6068
*/
61-
private fun enhanceRequestByExternalIdentityParam(
69+
private fun enhanceRequestByAdditionalParams(
6270
authorizationRequest: OAuth2AuthorizationRequest,
6371
exchange: ServerWebExchange?,
6472
): Mono<OAuth2AuthorizationRequest> = Mono.justOrEmpty(exchange)
@@ -72,11 +80,23 @@ class FederationAwareOauth2AuthorizationRequestResolver(
7280
additionalParams[COGNITO_EXTERNAL_PROVIDER_ID_PARAM_NAME] = externalIdp
7381
}
7482
.build()
83+
}.switchIfEmpty {
84+
Mono.just(authorizationRequest)
85+
}.flatMap { request ->
86+
getOrganizationFromContext().flatMap { organization ->
87+
Mono.just(OAuth2AuthorizationRequest.from(request)
88+
.additionalParameters { additionalParams ->
89+
// if organization contains additional authentication attributes, add them to the request
90+
organization.oauthCustomAuthAttributes?.takeIf { it.isNotEmpty() }
91+
?.forEach { (key, value) ->
92+
additionalParams[key] = value
93+
}
94+
}
95+
.build()
96+
)
97+
}
7598
}
7699
}
77-
.switchIfEmpty {
78-
Mono.just(authorizationRequest)
79-
}
80100

81101
companion object {
82102
/**
Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.gooddata.oauth2.server.oauth2.client
1717

18+
import com.gooddata.oauth2.server.CookieSerializerTest.Companion.ORGANIZATION
19+
import com.gooddata.oauth2.server.Organization
20+
import com.gooddata.oauth2.server.OrganizationWebFilter.Companion.orgContextWrite
1821
import com.gooddata.oauth2.server.ReactiveCookieService
1922
import com.gooddata.oauth2.server.SPRING_EXTERNAL_IDP
2023
import io.mockk.every
@@ -31,7 +34,7 @@ import reactor.core.publisher.Mono
3134
import reactor.test.StepVerifier
3235

3336
@Suppress("ReactiveStreamsUnusedPublisher")
34-
class FederationAwareOauth2AuthorizationRequestResolverTest {
37+
class CustomAttrsAwareOauth2AuthorizationRequestResolverTest {
3538

3639
private val defaultResolver: ServerOAuth2AuthorizationRequestResolver = mockk {
3740
every { resolve(any()) } returns Mono.just(DEFAULT_AUTH_REQUEST)
@@ -42,11 +45,14 @@ class FederationAwareOauth2AuthorizationRequestResolverTest {
4245
every { invalidateCookie(any(), any()) } just runs
4346
}
4447

45-
private val resolver = FederationAwareOauth2AuthorizationRequestResolver(defaultResolver, cookieService)
48+
private val resolver = CustomAttrsAwareOauth2AuthorizationRequestResolver(defaultResolver, cookieService)
4649

4750
@Test
4851
fun `resolve without cookie`() {
49-
StepVerifier.create(oauthRequestToUri(resolver.resolve(EXCHANGE)))
52+
StepVerifier.create(
53+
oauthRequestToUri(resolver.resolve(EXCHANGE))
54+
.orgContextWrite(ORGANIZATION)
55+
)
5056
.expectNext("https://example.com/oauth2/authorize?response_type=code&client_id=client-id")
5157
.verifyComplete()
5258
}
@@ -55,10 +61,16 @@ class FederationAwareOauth2AuthorizationRequestResolverTest {
5561
fun `resolve with cookie and invalidates it`() {
5662
every { cookieService.decodeCookie(EXCHANGE, SPRING_EXTERNAL_IDP) } returns Mono.just("external-idp-id")
5763

58-
StepVerifier.create(oauthRequestToUri(resolver.resolve(EXCHANGE)))
64+
StepVerifier.create(
65+
oauthRequestToUri(resolver.resolve(EXCHANGE))
66+
.orgContextWrite(ORGANIZATION_WITH_CUSTOM_AUTH_ATTRS)
67+
)
5968
.expectNext(
6069
"https://example.com/oauth2/authorize" +
61-
"?response_type=code&client_id=client-id&identity_provider=external-idp-id"
70+
"?response_type=code&client_id=client-id" +
71+
"&identity_provider=external-idp-id" +
72+
"&organization=org_vswH67L51ZQW67PS"
73+
6274
)
6375
.verifyComplete()
6476

@@ -67,11 +79,31 @@ class FederationAwareOauth2AuthorizationRequestResolverTest {
6779

6880
@Test
6981
fun `resolve without cookie and client registration id`() {
70-
StepVerifier.create(oauthRequestToUri(resolver.resolve(EXCHANGE, "registration-id")))
82+
StepVerifier.create(
83+
oauthRequestToUri(
84+
resolver.resolve(EXCHANGE, "registration-id")
85+
)
86+
.orgContextWrite(ORGANIZATION)
87+
)
7188
.expectNext("https://example.com/oauth2/authorize?response_type=code&client_id=client-id")
7289
.verifyComplete()
7390
}
7491

92+
@Test
93+
fun `resolve without cookie and client registration id with custom authentication attributes`() {
94+
StepVerifier.create(
95+
oauthRequestToUri(
96+
resolver.resolve(EXCHANGE, "registration-id")
97+
)
98+
.orgContextWrite(ORGANIZATION_WITH_CUSTOM_AUTH_ATTRS)
99+
)
100+
.expectNext(
101+
"https://example.com/oauth2/authorize" +
102+
"?response_type=code&client_id=client-id&organization=org_vswH67L51ZQW67PS"
103+
)
104+
.verifyComplete()
105+
}
106+
75107
companion object {
76108
private val DEFAULT_AUTH_REQUEST = OAuth2AuthorizationRequest
77109
.authorizationCode()
@@ -85,5 +117,8 @@ class FederationAwareOauth2AuthorizationRequestResolverTest {
85117

86118
private fun oauthRequestToUri(request: Mono<OAuth2AuthorizationRequest>): Mono<String> =
87119
request.map(OAuth2AuthorizationRequest::getAuthorizationRequestUri)
120+
121+
val ORGANIZATION_WITH_CUSTOM_AUTH_ATTRS =
122+
Organization(id = "org", oauthCustomAuthAttributes = mapOf("organization" to "org_vswH67L51ZQW67PS"))
88123
}
89124
}

0 commit comments

Comments
 (0)