diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManager.java index 6c3ea54be9..c4733bf5ad 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManager.java @@ -1,10 +1,12 @@ package org.cloudfoundry.identity.uaa.authentication.manager; import org.apache.commons.lang3.ObjectUtils; +import org.cloudfoundry.identity.uaa.authentication.AbstractClientParametersAuthenticationFilter; import org.cloudfoundry.identity.uaa.authentication.ProviderConfigurationException; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaLoginHint; import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; +import org.cloudfoundry.identity.uaa.constants.ClientAuthentication; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.impl.config.RestTemplateConfig; import org.cloudfoundry.identity.uaa.login.Prompt; @@ -132,9 +134,10 @@ Authentication oidcPasswordGrant(Authentication authentication, OIDCIdentityProv if (clientId == null) { throw new ProviderConfigurationException("External OpenID Connect provider configuration is missing relyingPartyId."); } - if (clientSecret == null && config.getJwtClientAuthentication() == null) { - throw new ProviderConfigurationException("External OpenID Connect provider configuration is missing relyingPartySecret or jwtclientAuthentication."); + if (clientSecret == null && config.getJwtClientAuthentication() == null && config.getAuthMethod() == null) { + throw new ProviderConfigurationException("External OpenID Connect provider configuration is missing relyingPartySecret, jwtClientAuthentication or authMethod."); } + String calcAuthMethod = ClientAuthentication.getCalculatedMethod(config.getAuthMethod(), clientSecret != null, config.getJwtClientAuthentication() != null); String userName = authentication.getPrincipal() instanceof String ? (String)authentication.getPrincipal() : null; if (userName == null || authentication.getCredentials() == null || !(authentication.getCredentials() instanceof String)) { throw new BadCredentialsException("Request is missing username or password."); @@ -152,12 +155,14 @@ Authentication oidcPasswordGrant(Authentication authentication, OIDCIdentityProv headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap params = new LinkedMultiValueMap<>(); - if (clientSecret == null) { + if (ClientAuthentication.PRIVATE_KEY_JWT.equals(calcAuthMethod)) { params = new JwtClientAuthentication(externalOAuthAuthenticationManager.getKeyInfoService()) .getClientAuthenticationParameters(params, config); - } else { + } else if (ClientAuthentication.secretNeeded(calcAuthMethod)){ String auth = clientId + ":" + clientSecret; headers.add("Authorization", "Basic " + Base64Utils.encodeToString(auth.getBytes())); + } else { + params.add(AbstractClientParametersAuthenticationFilter.CLIENT_ID, clientId); } if (config.isSetForwardHeader() && authentication.getDetails() != null &&authentication.getDetails() instanceof UaaAuthenticationDetails) { UaaAuthenticationDetails details = (UaaAuthenticationDetails) authentication.getDetails(); 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 fabc4a17cc..9158f59032 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 @@ -23,6 +23,7 @@ import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent; import org.cloudfoundry.identity.uaa.authentication.manager.ExternalLoginAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent; +import org.cloudfoundry.identity.uaa.constants.ClientAuthentication; import org.cloudfoundry.identity.uaa.oauth.KeyInfo; import org.cloudfoundry.identity.uaa.oauth.KeyInfoService; import org.cloudfoundry.identity.uaa.oauth.TokenEndpointBuilder; @@ -712,7 +713,8 @@ private String getTokenFromCode(ExternalOAuthCodeToken codeToken, AbstractExtern // https://docs.spring.io/spring-security/site/docs/5.3.1.RELEASE/reference/html5/#initiating-the-authorization-request if (config.getRelyingPartySecret() == null) { // no secret but jwtClientAuthentication - if (config instanceof OIDCIdentityProviderDefinition && ((OIDCIdentityProviderDefinition) config).getJwtClientAuthentication() != null) { + if (config instanceof OIDCIdentityProviderDefinition oidcDefinition && ClientAuthentication.PRIVATE_KEY_JWT.equals( + ClientAuthentication.getCalculatedMethod(config.getAuthMethod(), false, oidcDefinition.getJwtClientAuthentication() != null))) { body = new JwtClientAuthentication(keyInfoService) .getClientAuthenticationParameters(body, (OIDCIdentityProviderDefinition) config); } 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 58eec04ebc..f3de0fba78 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 @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.provider.oauth; +import org.cloudfoundry.identity.uaa.constants.ClientAuthentication; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.login.Prompt; import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; @@ -177,6 +178,13 @@ protected void setCommonProperties(Map idpDefinitionMap, Abstrac if (idpDefinitionMap.get("cacheJwks") instanceof Boolean) { idpDefinition.setCacheJwks((boolean)idpDefinitionMap.get("cacheJwks")); } + if (idpDefinitionMap.get("authMethod") instanceof String definedAuthMethod) { + if (ClientAuthentication.isMethodSupported(definedAuthMethod)) { + idpDefinition.setAuthMethod(definedAuthMethod); + } else { + throw new IllegalArgumentException("Invalid IdP authentication method"); + } + } } private static Map parseAdditionalParameters(Map idpDefinitionMap) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManagerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManagerTest.java index 02d6f03317..c1bf8e884f 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManagerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PasswordGrantAuthenticationManagerTest.java @@ -51,6 +51,8 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.text.ParseException; import java.util.ArrayList; @@ -876,6 +878,32 @@ void oidcPasswordGrant_credentialsMustBeString() { assertThrows(BadCredentialsException.class, () -> instance.oidcPasswordGrant(authentication, config)); } + @Test + void oidcPasswordGrant_credentialsMustBeStringButNoSecretNeeded() throws MalformedURLException { + RestTemplate restTemplate = mock(RestTemplate.class); + ResponseEntity responseEntity = mock(ResponseEntity.class); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("user", ""); + OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition() + .setRelyingPartyId("client-id").setTokenUrl(URI.create("http://localhost:8080/uaa/oauth/token").toURL()); + config.setAuthMethod("none"); + OIDCIdentityProviderDefinition spyConfig = spy(config); + when(restTemplateConfig.nonTrustingRestTemplate()).thenReturn(restTemplate); + when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenReturn(responseEntity); + when(responseEntity.hasBody()).thenReturn(true); + when(responseEntity.getBody()).thenReturn(Map.of("id_token", "dummy")); + assertNull(instance.oidcPasswordGrant(authentication, spyConfig)); + verify(spyConfig, atLeast(2)).getAuthMethod(); + } + + @Test + void oidcPasswordGrant_requireAuthenticationStatement() { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("user", new Object()); + OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition() + .setRelyingPartyId("client-id"); + Exception exception = assertThrows(ProviderConfigurationException.class, () -> instance.oidcPasswordGrant(authentication, config)); + assertEquals("External OpenID Connect provider configuration is missing relyingPartySecret, jwtClientAuthentication or authMethod.", exception.getMessage()); + } + private void mockKeyInfoService() throws JOSEException { KeyInfoService keyInfoService = mock(KeyInfoService.class); KeyInfo keyInfo = mock(KeyInfo.class); 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 c79a8327cb..8f6999b6c7 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 @@ -15,6 +15,7 @@ package org.cloudfoundry.identity.uaa.provider.oauth; +import org.cloudfoundry.identity.uaa.constants.ClientAuthentication; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.junit.Before; @@ -33,6 +34,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class OauthIdentityProviderDefinitionFactoryBeanTest { @@ -268,4 +270,34 @@ public void testPerformRpInitiatedLogoutFalse() { factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition); assertFalse(providerDefinition.isPerformRpInitiatedLogout()); } + + @Test + public void testAuthMethodNotSet() { + factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition); + assertNull(providerDefinition.getAuthMethod()); + assertEquals(ClientAuthentication.CLIENT_SECRET_BASIC, ClientAuthentication.getCalculatedMethod(providerDefinition.getAuthMethod(), providerDefinition.getRelyingPartySecret() != null, providerDefinition.getJwtClientAuthentication() != null)); + } + + @Test + public void testAuthMethodSetInvalidValue() { + idpDefinitionMap.put("authMethod", "empty"); + assertThrows(IllegalArgumentException.class, () -> factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition)); + } + + @Test + public void testAuthMethodSet() { + // given: jwtclientAuthentication, but overrule it with authMethod=none + idpDefinitionMap.put("jwtclientAuthentication", true); + idpDefinitionMap.put("authMethod", "none"); + idpDefinitionMap.put("type", OriginKeys.OIDC10); + Map definitions = new HashMap<>(); + definitions.put("new.idp", idpDefinitionMap); + // when: load beans from uaa.yml + factoryBean = new OauthIDPWrapperFactoryBean(definitions); + factoryBean.setCommonProperties(idpDefinitionMap, providerDefinition); + // then + assertTrue(factoryBean.getProviders().get(0).getProvider().getConfig() instanceof OIDCIdentityProviderDefinition); + assertNotNull(((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getJwtClientAuthentication()); + assertEquals("none", (((OIDCIdentityProviderDefinition) factoryBean.getProviders().get(0).getProvider().getConfig()).getAuthMethod())); + } }