From e3367a0ad5cf3347193b0de7bc166c969bf435c0 Mon Sep 17 00:00:00 2001 From: Markus Strehle <11627201+strehle@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:42:14 +0200 Subject: [PATCH] feature: Allow sending static key/value pairs to the configured IdP (#2397) * Feature: Allow sending static key/value pairs to the configured external IdP for authorization code flow Add parameter additionalAuthzParameters (additional authorization parameters) to requests for auth code flow. The usage is: OIDC IdPs have additional parameters, e.g. UAA itself has token_format to get special features. The new parameter allows to customize the IdP with own parameters * Documentation added * tests * more tests * review * add test * cleanup * test converage * Feature: Allow sending static key/value pairs to the configured external IdP for authorization code flow Add parameter additionalAuthzParameters (additional authorization parameters) to requests for auth code flow. The usage is: OIDC IdPs have additional parameters, e.g. UAA itself has token_format to get special features. The new parameter allows to customize the IdP with own parameters * sonar * rebase * review * review * review --- .../OIDCIdentityProviderDefinition.java | 18 +++++++- .../OIDCIdentityProviderDefinitionTests.java | 15 ++++++- .../ExternalOAuthAuthenticationManager.java | 9 ++++ ...lOAuthIdentityProviderConfigValidator.java | 41 ++++++++++++++----- .../ExternalOAuthProviderConfigurator.java | 5 +++ .../oauth/OauthIDPWrapperFactoryBean.java | 38 +++++++++++++++-- .../ExternalOAuthAuthenticationManagerIT.java | 17 ++++++++ ...thIdentityProviderConfigValidatorTest.java | 24 +++++++++++ ...xternalOAuthProviderConfiguratorTests.java | 10 +++++ ...tityProviderDefinitionFactoryBeanTest.java | 37 +++++++++++++++++ .../IdentityProviderEndpointDocs.java | 1 + 11 files changed, 198 insertions(+), 17 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinition.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinition.java index 6595ae98e46..3173b532db6 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinition.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinition.java @@ -17,9 +17,13 @@ import org.cloudfoundry.identity.uaa.login.Prompt; import java.net.URL; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import static java.util.Collections.emptyMap; @JsonIgnoreProperties(ignoreUnknown = true) public class OIDCIdentityProviderDefinition extends AbstractExternalOAuthIdentityProviderDefinition @@ -31,6 +35,8 @@ public class OIDCIdentityProviderDefinition extends AbstractExternalOAuthIdentit private List prompts = null; @JsonInclude(JsonInclude.Include.NON_NULL) private Object jwtClientAuthentication; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Map additionalAuthzParameters = null; public URL getDiscoveryUrl() { return discoveryUrl; @@ -74,6 +80,14 @@ public void setJwtClientAuthentication(final Object jwtClientAuthentication) { this.jwtClientAuthentication = jwtClientAuthentication; } + public Map getAdditionalAuthzParameters() { + return this.additionalAuthzParameters != null ? Collections.unmodifiableMap(this.additionalAuthzParameters) : null; + } + + public void setAdditionalAuthzParameters(final Map additonalAuthzParameters) { + this.additionalAuthzParameters = new HashMap<>(additonalAuthzParameters!=null?additonalAuthzParameters: emptyMap()); + } + @Override public Object clone() throws CloneNotSupportedException { return super.clone(); @@ -89,7 +103,8 @@ public boolean equals(Object o) { if (this.passwordGrantEnabled != that.passwordGrantEnabled) return false; if (this.setForwardHeader != that.setForwardHeader) return false; - if (this.jwtClientAuthentication != that.jwtClientAuthentication) return false; + if (!Objects.equals(this.jwtClientAuthentication, that.jwtClientAuthentication)) return false; + if (!Objects.equals(this.additionalAuthzParameters, that.additionalAuthzParameters)) return false; return Objects.equals(discoveryUrl, that.discoveryUrl); } @@ -101,6 +116,7 @@ public int hashCode() { result = 31 * result + (passwordGrantEnabled ? 1 : 0); result = 31 * result + (setForwardHeader ? 1 : 0); result = 31 * result + (jwtClientAuthentication != null ? jwtClientAuthentication.hashCode() : 0); + result = 31 * result + (additionalAuthzParameters != null ? additionalAuthzParameters.hashCode() : 0); return result; } } diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java index 098c2aaaa37..ff4f0f6cf44 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/OIDCIdentityProviderDefinitionTests.java @@ -28,11 +28,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; - +import static org.junit.Assert.assertTrue; public class OIDCIdentityProviderDefinitionTests { - private final String defaultJson = "{\"emailDomain\":null,\"additionalConfiguration\":null,\"providerDescription\":null,\"externalGroupsWhitelist\":[],\"attributeMappings\":{},\"addShadowUserOnLogin\":true,\"storeCustomAttributes\":false,\"authUrl\":null,\"tokenUrl\":null,\"tokenKeyUrl\":null,\"tokenKey\":null,\"linkText\":null,\"showLinkText\":true,\"skipSslValidation\":false,\"relyingPartyId\":null,\"relyingPartySecret\":null,\"scopes\":null,\"issuer\":null,\"responseType\":\"code\",\"userInfoUrl\":null,\"jwtClientAuthentication\":false}"; + private final String defaultJson = "{\"emailDomain\":null,\"additionalConfiguration\":null,\"providerDescription\":null,\"externalGroupsWhitelist\":[],\"attributeMappings\":{},\"addShadowUserOnLogin\":true,\"storeCustomAttributes\":false,\"authUrl\":null,\"tokenUrl\":null,\"tokenKeyUrl\":null,\"tokenKey\":null,\"linkText\":null,\"showLinkText\":true,\"skipSslValidation\":false,\"relyingPartyId\":null,\"relyingPartySecret\":null,\"scopes\":null,\"issuer\":null,\"responseType\":\"code\",\"userInfoUrl\":null,\"jwtClientAuthentication\":false,\"additionalAuthzParameters\":{\"token_format\":\"jwt\"}}"; String url = "https://accounts.google.com/.well-known/openid-configuration"; @Test @@ -44,6 +44,17 @@ public void serialize_discovery_url() throws MalformedURLException { String json = JsonUtils.writeValueAsString(def); def = JsonUtils.readValue(json, OIDCIdentityProviderDefinition.class); assertEquals(url, def.getDiscoveryUrl().toString()); + assertEquals("jwt", def.getAdditionalAuthzParameters().get("token_format")); + } + + @Test + public void testSerializableObjectCalls() throws CloneNotSupportedException { + OIDCIdentityProviderDefinition def = JsonUtils.readValue(defaultJson, OIDCIdentityProviderDefinition.class); + OIDCIdentityProviderDefinition def2 = (OIDCIdentityProviderDefinition) def.clone(); + assertTrue(def.equals(def2)); + assertEquals(def.hashCode(), def2.hashCode()); + assertEquals(1, def2.getAdditionalAuthzParameters().size()); + assertEquals("jwt", def2.getAdditionalAuthzParameters().get("token_format")); } @Test diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java index 1defc017d29..0d6c3a367c9 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java @@ -690,6 +690,15 @@ private String getTokenFromCode(ExternalOAuthCodeToken codeToken, AbstractExtern logger.debug("Adding new client_id and client_secret for token exchange"); body.add("client_id", config.getRelyingPartyId()); + if (config instanceof OIDCIdentityProviderDefinition) { + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) config; + if (oidcIdentityProviderDefinition.getAdditionalAuthzParameters() != null){ + for (Map.Entry entry : oidcIdentityProviderDefinition.getAdditionalAuthzParameters().entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } + } + } + HttpHeaders headers = new HttpHeaders(); // no client-secret, switch to PKCE diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java index facd699a1be..0ab99a1a811 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java @@ -1,19 +1,26 @@ package org.cloudfoundry.identity.uaa.provider.oauth; -import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.BaseIdentityProviderValidator; import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.springframework.stereotype.Component; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.Set; import static org.springframework.util.StringUtils.hasText; @Component public class ExternalOAuthIdentityProviderConfigValidator extends BaseIdentityProviderValidator { + private static final Set oAuthStandardParameters = Set.of("redirect_uri", "code", "client_id", "client_secret", "response_type", + "grant_type", "code_verifier", "client_assertion", "client_assertion_type", "code_challenge", "code_challenge_method", "nonce", "state", + "scope", "assertion", "subject_token", "actor_token", "username", "password"); + @Override public void validate(AbstractIdentityProviderDefinition definition) { if (definition == null) { @@ -26,19 +33,27 @@ public void validate(AbstractIdentityProviderDefinition definition) { AbstractExternalOAuthIdentityProviderDefinition def = (AbstractExternalOAuthIdentityProviderDefinition) definition; List errors = new ArrayList<>(); - if (def instanceof OIDCIdentityProviderDefinition && ((OIDCIdentityProviderDefinition) definition).getDiscoveryUrl() != null) { - //we don't require auth/token url or keys/key url - } else { - if (def.getAuthUrl() == null) { - errors.add("Authorization URL must be a valid URL"); - } + if (def instanceof OIDCIdentityProviderDefinition) { + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) def; + if (oidcIdentityProviderDefinition.getDiscoveryUrl() != null) { + //we don't require auth/token url or keys/key url + } else { + if (def.getAuthUrl() == null) { + errors.add("Authorization URL must be a valid URL"); + } - if (def.getTokenUrl() == null) { - errors.add("Token URL must be a valid URL"); + if (def.getTokenUrl() == null) { + errors.add("Token URL must be a valid URL"); + } + + if (!hasText(def.getTokenKey()) && def.getTokenKeyUrl() == null) { + errors.add("Either token key or token key URL must be specified"); + } } - if (!hasText(def.getTokenKey()) && def.getTokenKeyUrl() == null) { - errors.add("Either token key or token key URL must be specified"); + if (Optional.ofNullable(oidcIdentityProviderDefinition.getAdditionalAuthzParameters()).orElse(Collections.emptyMap()) + .keySet().stream().anyMatch(ExternalOAuthIdentityProviderConfigValidator::isOAuthStandardParameter)) { + errors.add("No OAuth standard parameters allowed in section additionalAuthzParameters"); } } @@ -56,4 +71,8 @@ public void validate(AbstractIdentityProviderDefinition definition) { throw new IllegalArgumentException("Invalid config for Identity Provider " + errorMessages); } } + + protected static boolean isOAuthStandardParameter(String value) { + return oAuthStandardParameters.contains(value); + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java index b05b4c3c94b..b51d6b6080c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java @@ -22,9 +22,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Optional.ofNullable; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -92,6 +94,9 @@ public String getIdpAuthenticationUrl( if (OIDCIdentityProviderDefinition.class.equals(definition.getParameterizedClass())) { var nonceGenerator = new RandomValueStringGenerator(12); uriBuilder.queryParam("nonce", nonceGenerator.generate()); + + Map additionalParameters = ofNullable(((OIDCIdentityProviderDefinition) definition).getAdditionalAuthzParameters()).orElse(emptyMap()); + additionalParameters.keySet().stream().forEach(e -> uriBuilder.queryParam(e, additionalParameters.get(e))); } return uriBuilder.build().toUriString(); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java index e1477aae279..887f957c844 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; @@ -142,9 +143,17 @@ protected void setCommonProperties(Map idpDefinitionMap, Abstrac } String discoveryUrl = (String) idpDefinitionMap.get("discoveryUrl"); try { - if (hasText(discoveryUrl) && idpDefinition instanceof OIDCIdentityProviderDefinition) { - ((OIDCIdentityProviderDefinition) idpDefinition).setDiscoveryUrl(new URL(discoveryUrl)); - } else { + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = null; + if (idpDefinition instanceof OIDCIdentityProviderDefinition) { + oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) idpDefinition; + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(parseAdditionalParameters(idpDefinitionMap)); + + if (hasText(discoveryUrl)) { + oidcIdentityProviderDefinition.setDiscoveryUrl(new URL(discoveryUrl)); + } + } + + if (oidcIdentityProviderDefinition == null || !hasText(discoveryUrl)) { idpDefinition.setAuthUrl(new URL((String) idpDefinitionMap.get("authUrl"))); idpDefinition.setTokenKeyUrl(idpDefinitionMap.get("tokenKeyUrl") == null ? null : new URL((String) idpDefinitionMap.get("tokenKeyUrl"))); idpDefinition.setTokenUrl(new URL((String) idpDefinitionMap.get("tokenUrl"))); @@ -159,6 +168,29 @@ protected void setCommonProperties(Map idpDefinitionMap, Abstrac } } + private static Map parseAdditionalParameters(Map idpDefinitionMap) { + Map additionalParameters = (Map) idpDefinitionMap.get("additionalAuthzParameters"); + if (additionalParameters != null) { + Map additionalQueryParameters = new HashMap<>(); + for (Map.Entry entry : additionalParameters.entrySet()) { + String keyEntry = entry.getKey().toLowerCase(Locale.ROOT); + String value = null; + if (entry.getValue() instanceof Integer) { + value = String.valueOf(entry.getValue()); + } else if (entry.getValue() instanceof String) { + value = (String) entry.getValue(); + } + // accept only custom parameters, filter out standard parameters + if (value == null || ExternalOAuthIdentityProviderConfigValidator.isOAuthStandardParameter(keyEntry)) { + continue; + } + additionalQueryParameters.put(entry.getKey(), value); + } + return additionalQueryParameters; + } + return null; + } + /* parse with null check because default should be null */ private AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode parseExternalGroupMappingMode(Object mode) { if (mode instanceof String) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java index 8f64123ec1b..53a86c95fbf 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java @@ -652,6 +652,23 @@ void pkceWithJwtClientAuthInBody_is_used() { mockUaaServer.verify(); } + @Test + void testAdditionalParameterClientAuthInBody_is_used() { + config.setClientAuthInBody(true); + config.setAdditionalAuthzParameters(Map.of("token_format", "opaque")); + mockUaaServer.expect(requestTo(config.getTokenUrl().toString())) + .andExpect(request -> assertThat("Check Auth header not present", request.getHeaders().get("Authorization"), nullValue())) + .andExpect(content().string(containsString("token_format=opaque"))) + .andExpect(content().string(containsString("client_id=" + config.getRelyingPartyId()))) + .andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(getIdTokenResponse())); + IdentityProvider identityProvider = getProvider(); + when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider); + Map idToken = externalOAuthAuthenticationManager.getClaimsFromToken(xCodeToken, config); + assertNotNull(idToken); + + mockUaaServer.verify(); + } + @Test void idToken_In_Redirect_Should_Use_it() { mockToken(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidatorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidatorTest.java index a01e7e28fc3..24a1ee9973f 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidatorTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidatorTest.java @@ -8,6 +8,8 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.Collections; +import java.util.Map; public class ExternalOAuthIdentityProviderConfigValidatorTest { private AbstractExternalOAuthIdentityProviderDefinition definition; @@ -102,4 +104,26 @@ public void tokenKeyUrl_orTokenKeyMustBeSpecified() { definition.setTokenKeyUrl(null); validator.validate(definition); } + + @Test + public void testAdditionalParametersAdd() { + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) definition; + // nothing + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(null); + validator.validate(definition); + // empty + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(Collections.emptyMap()); + validator.validate(definition); + // list + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(Map.of("token_format", "jwt", "token_key", "any")); + validator.validate(definition); + } + + @Test(expected = IllegalArgumentException.class) + public void testAdditionalParametersError() { + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) definition; + // one standard parameter, should fail + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(Map.of("token_format", "jwt", "code", "1234")); + validator.validate(definition); + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java index 9f0c8c8af50..a54dac050de 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java @@ -89,6 +89,7 @@ void setup() throws MalformedURLException { def.setRelyingPartySecret("clientSecret"); } oidc.setResponseType("id_token code"); + oidc.setAdditionalAuthzParameters(Map.of("token_format", "jwt")); oauth.setResponseType("code"); configurator = spy(new ExternalOAuthProviderConfigurator( @@ -330,4 +331,13 @@ void excludeUnreachableOidcProvider() throws OidcMetadataFetchingException { assertEquals(oauthProvider.getName(), providers.get(0).getName()); verify(configurator, times(1)).overlay(eq(config)); } + + @Test + void testGetIdpAuthenticationUrlAndCheckTokenFormatParameter() { + String authzUri = configurator.getIdpAuthenticationUrl(oidc, OIDC10, mockHttpServletRequest); + + Map queryParams = + UriComponentsBuilder.fromUriString(authzUri).build().getQueryParams().toSingleValueMap(); + assertThat(queryParams, hasEntry("token_format", "jwt")); + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIdentityProviderDefinitionFactoryBeanTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIdentityProviderDefinitionFactoryBeanTest.java index 2202fc7744a..ff7a1ec05b4 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIdentityProviderDefinitionFactoryBeanTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIdentityProviderDefinitionFactoryBeanTest.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.STORE_CUSTOM_ATTRIBUTES_NAME; @@ -197,4 +198,40 @@ public void testDiscoveryUrl() { assertNull(((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getTokenUrl()); assertNull(((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getLogoutUrl()); } + + @Test + public void testAdditionalParametersInConfig() { + Map additionalMap = new HashMap<>(); + Map definitions = new HashMap<>(); + additionalMap.put("token_format", "jwt"); + additionalMap.put("expires", 0); + additionalMap.put("code", 12345678); + additionalMap.put("client_id", "id"); + additionalMap.put("complex", Set.of("1", "2")); + additionalMap.put("null", null); + additionalMap.put("empty", ""); + idpDefinitionMap.put("additionalAuthzParameters", additionalMap); + idpDefinitionMap.put("type", OriginKeys.OIDC10); + definitions.put("test", idpDefinitionMap); + factoryBean = new OauthIDPWrapperFactoryBean(definitions); + factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition); + assertTrue(factoryBean.getProviders().get(0).getProvider().getConfig() instanceof OIDCIdentityProviderDefinition); + Map receivedParameters = ((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getAdditionalAuthzParameters(); + assertEquals(3, receivedParameters.size()); + assertEquals("jwt", receivedParameters.get("token_format")); + assertEquals("0", receivedParameters.get("expires")); + assertEquals("", receivedParameters.get("empty")); + } + + @Test + public void testNoAdditionalParametersInConfig() { + Map definitions = new HashMap<>(); + idpDefinitionMap.put("type", OriginKeys.OIDC10); + definitions.put("test", idpDefinitionMap); + factoryBean = new OauthIDPWrapperFactoryBean(definitions); + factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition); + assertTrue(factoryBean.getProviders().get(0).getProvider().getConfig() instanceof OIDCIdentityProviderDefinition); + Map receivedParameters = ((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getAdditionalAuthzParameters(); + assertEquals(0, receivedParameters.size()); + } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index b217f20238d..fc69b2718c3 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -561,6 +561,7 @@ void createOidcIdentityProvider() throws Exception { fieldWithPath("config.setForwardHeader").optional(false).type(BOOLEAN).description("Only effective if Password Grant enabled. Set X-Forward-For header in Password Grant request to this identity provider."), fieldWithPath("config.jwtClientAuthentication").optional(null).type(OBJECT).description("UAA 76.5.0 Only effective if relyingPartySecret is not set or null. Creates private_key_jwt client authentication according to OIDC or OAuth2 (RFC 7523) standard. For OIDC, set true. For OAuth2, define custom elements `iss` and `aud`."), fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`."), + fieldWithPath("config.additionalAuthzParameters").optional(null).type(OBJECT).description("UAA 76.17.0Map of key-value pairs that are added as additional parameters for grant type `authorization_code`. For example, configure an entry with key `token_format` and value `jwt`."), fieldWithPath("config.prompts[]").optional(null).type(ARRAY).description("List of fields that users are prompted on to the OIDC provider through the password grant flow. Defaults to username, password, and passcode. Any additional prompts beyond username, password, and passcode will be forwarded on to the OIDC provider."), fieldWithPath("config.prompts[].name").optional(null).type(STRING).description("Name of field"), fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"),