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..5b37f25f636 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(); @@ -90,6 +104,7 @@ 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 (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/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/ExternalOAuthProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java index b05b4c3c94b..1c12a55b666 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,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Objects; 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 +95,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().filter(Objects::nonNull).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..fca34552884 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,7 +26,9 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -37,6 +39,9 @@ public class OauthIDPWrapperFactoryBean { private Map oauthIdpDefinitions = new HashMap<>(); private List providers = new LinkedList<>(); + private static final Set oauthParameters = 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"); public OauthIDPWrapperFactoryBean(Map definitions) { if (definitions != null) { @@ -142,8 +147,13 @@ 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)); + OIDCIdentityProviderDefinition oidcIdentityProviderDefinition = null; + if (idpDefinition instanceof OIDCIdentityProviderDefinition) { + oidcIdentityProviderDefinition = (OIDCIdentityProviderDefinition) idpDefinition; + oidcIdentityProviderDefinition.setAdditionalAuthzParameters(parseAdditionalParameters(idpDefinitionMap)); + } + if (hasText(discoveryUrl) && oidcIdentityProviderDefinition != null) { + oidcIdentityProviderDefinition.setDiscoveryUrl(new URL(discoveryUrl)); } else { idpDefinition.setAuthUrl(new URL((String) idpDefinitionMap.get("authUrl"))); idpDefinition.setTokenKeyUrl(idpDefinitionMap.get("tokenKeyUrl") == null ? null : new URL((String) idpDefinitionMap.get("tokenKeyUrl"))); @@ -159,6 +169,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 + if (value == null || oauthParameters.contains(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..1b264e8c210 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 @@ -263,6 +263,7 @@ void setUp() throws Exception { .setRelyingPartySecret("identitysecret") .setUserInfoUrl(new URL("http://localhost/userinfo")) .setTokenKey(PUBLIC_KEY); + config.setAdditionalAuthzParameters(Map.of("token_format", "jwt")); config.setExternalGroupsWhitelist( Collections.singletonList( "*" diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java index 23c8374d61d..ca9e25e4c71 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java @@ -135,6 +135,7 @@ public void setup() throws Exception { entry(GROUP_ATTRIBUTE_NAME, "roles") ); oidcConfig.setAttributeMappings(externalGroupMapping); + oidcConfig.setAdditionalAuthzParameters(Map.of("token_format", "jwt")); provider.setConfig(oidcConfig); when(identityProviderProvisioning.retrieveByOrigin(origin, zoneId)).thenReturn(provider); uaaIssuerBaseUrl = "http://uaa.example.com"; 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..f9c8489a625 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 @@ -107,6 +107,7 @@ void setup() throws MalformedURLException { config.setRelyingPartySecret("identitysecret"); config.setResponseType("id_token"); config.setScopes(List.of("openid", "cloud_controller.read")); + config.setAdditionalAuthzParameters(Map.of("token_format", "jwt")); oidcProvider = new IdentityProvider<>(); oidcProvider.setType(OIDC10); 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 10bdc4534b4..7b2b22aaf13 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; @@ -165,4 +166,40 @@ public void jwtClientAuthenticationWithCustomSetting() { assertNotNull(((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getJwtClientAuthentication()); assertEquals("issuer", (((Map)((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getJwtClientAuthentication()).get("iss"))); } -} \ No newline at end of file + + @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()); + } +}