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 fa2c4c3b17c..70b8ed5f192 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 @@ -72,33 +72,82 @@ public Authentication authenticate(Authentication authentication) throws Authent UaaLoginHint uaaLoginHint = zoneAwareAuthzAuthenticationManager.extractLoginHint(authentication); List allowedProviders = getAllowedProviders(); String defaultProvider = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider(); - UaaLoginHint loginHintToUse; + + // check whether there is a single OIDC IdP that qualifies for the login hint (or default origin as fallback) IdentityProvider identityProvider = retrieveOidcPasswordIdp(uaaLoginHint, defaultProvider, allowedProviders); - List possibleProviders; + + // determine the possible providers, i.e., those allowed for the client, supporting password grant and active + final List possibleProviders; if (identityProvider != null) { possibleProviders = List.of(identityProvider.getOriginKey()); } else { - List identityProviders = identityProviderProvisioning.retrieveActive(IdentityZoneHolder.get().getId()).stream().filter(this::providerSupportsPasswordGrant).map(IdentityProvider::getOriginKey).toList(); - possibleProviders = Optional.ofNullable(allowedProviders).orElse(identityProviders).stream().filter(identityProviders::contains).toList(); + /* no suiting OIDC IdP was found - get all qualifying IdPs in the zone + * (i.e., active, supports password grant and is allowed by the client) */ + final List identityProviders; + + final String originLoginHint = Optional.ofNullable(uaaLoginHint).map(UaaLoginHint::getOrigin).orElse(null); + final boolean isLoginHintUaa = OriginKeys.UAA.equalsIgnoreCase(originLoginHint); + if (isLoginHintUaa || OriginKeys.LDAP.equalsIgnoreCase(originLoginHint)) { + /* if "uaa" or "ldap" is passed in the login hint (not as default origin), only look up the single IdP + * instead of all qualifying ones in the zone (we later only allow this exact IdP anyway) */ + + // only returns active IdP + final IdentityProvider uaaOrLdapIdp = identityProviderProvisioning.retrieveByOrigin( + isLoginHintUaa ? OriginKeys.UAA : OriginKeys.LDAP, + IdentityZoneHolder.get().getId() + ); + + identityProviders = Optional.ofNullable(uaaOrLdapIdp) + .filter(PasswordGrantAuthenticationManager::providerSupportsPasswordGrant) // always true for "uaa" or "ldap" IdPs + .map(IdentityProvider::getOriginKey) + .stream() + .toList(); + } else { + identityProviders = identityProviderProvisioning.retrieveActive(IdentityZoneHolder.get().getId()) + .stream() + .filter(PasswordGrantAuthenticationManager::providerSupportsPasswordGrant) + .map(IdentityProvider::getOriginKey) + .toList(); + } + + // only keep the IdPs that are allowed by the client + if (allowedProviders == null) { + // client allows all IdPs + possibleProviders = new ArrayList<>(identityProviders); + } else { + possibleProviders = allowedProviders.stream().filter(identityProviders::contains).toList(); + } } - if (uaaLoginHint == null) { + + // determine the IdP to use from the list of possible ones + UaaLoginHint loginHintToUse; + if (uaaLoginHint == null || uaaLoginHint.getOrigin() == null) { if (defaultProvider != null && possibleProviders.contains(defaultProvider)) { + // no login hint was passed, but the default provider qualifies loginHintToUse = new UaaLoginHint(defaultProvider); } else { + /* no login hint was passed and there is either no default IdP or it does not qualify + * -> select a different IdP from the list of possible ones */ loginHintToUse = getUaaLoginHintForChainedAuth(possibleProviders); if (identityProvider == null) { identityProvider = retrieveOidcPasswordIdp(loginHintToUse, null, null); } } } else { + /* there is a login hint passed in the password grant request + * -> we must use exactly this IdP, but only if it is in the list of possible IdPs */ + if (possibleProviders.contains(uaaLoginHint.getOrigin())) { loginHintToUse = uaaLoginHint; - } else if (allowedProviders == null || allowedProviders.contains(uaaLoginHint.getOrigin())){ + } else if (allowedProviders == null || allowedProviders.contains(uaaLoginHint.getOrigin())) { + // login with the provider is not possible because it is not active or does not support password grant throw new ProviderConfigurationException("The origin provided in the login_hint does not match an active Identity Provider, that supports password grant."); } else { + // login with the provider is not possible because it is not allowed throw new ProviderConfigurationException("Client is not authorized for specified user's identity provider."); } } + if (loginHintToUse != null) { zoneAwareAuthzAuthenticationManager.setLoginHint(authentication, loginHintToUse); } @@ -110,19 +159,22 @@ public Authentication authenticate(Authentication authentication) throws Authent } private IdentityProvider retrieveOidcPasswordIdp(UaaLoginHint loginHint, String defaultOrigin, List allowedProviders) { - IdentityProvider idp = null; String useOrigin = loginHint != null && loginHint.getOrigin() != null ? loginHint.getOrigin() : defaultOrigin; - if (useOrigin != null && !useOrigin.equalsIgnoreCase(OriginKeys.UAA) && !useOrigin.equalsIgnoreCase(OriginKeys.LDAP)) { - try { - IdentityProvider retrievedByOrigin = externalOAuthProviderProvisioning.retrieveByOrigin(useOrigin, - IdentityZoneHolder.get().getId()); - if (retrievedByOrigin != null && retrievedByOrigin.isActive() && retrievedByOrigin.getOriginKey().equals(useOrigin) - && providerSupportsPasswordGrant(retrievedByOrigin) && (allowedProviders == null || allowedProviders.contains(useOrigin))) { - idp = retrievedByOrigin; - } - } catch (EmptyResultDataAccessException e) { - // ignore + + if (useOrigin == null || useOrigin.equalsIgnoreCase(OriginKeys.UAA) || useOrigin.equalsIgnoreCase(OriginKeys.LDAP)) { + return null; + } + + IdentityProvider idp = null; + try { + IdentityProvider retrievedByOrigin = externalOAuthProviderProvisioning.retrieveByOrigin(useOrigin, + IdentityZoneHolder.get().getId()); + if (retrievedByOrigin != null && retrievedByOrigin.isActive() && retrievedByOrigin.getOriginKey().equals(useOrigin) + && providerSupportsPasswordGrant(retrievedByOrigin) && (allowedProviders == null || allowedProviders.contains(useOrigin))) { + idp = retrievedByOrigin; } + } catch (EmptyResultDataAccessException e) { + // ignore } return idp; } @@ -246,7 +298,7 @@ Authentication oidcPasswordGrant(Authentication authentication, final IdentityPr return externalOAuthAuthenticationManager.authenticate(token); } - private boolean providerSupportsPasswordGrant(IdentityProvider provider) { + private static boolean providerSupportsPasswordGrant(IdentityProvider provider) { if (OriginKeys.UAA.equals(provider.getType()) || OriginKeys.LDAP.equals(provider.getType())) { return true; } 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 689d44cb41f..2bd6aefbd14 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 @@ -31,9 +31,12 @@ import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.ParameterizedTypeReference; @@ -117,6 +120,8 @@ void setUp() throws Exception { when(identityProviderProvisioning.retrieveActive("uaa")).thenReturn(Arrays.asList(idp, uaaProvider, ldapProvider)); when(externalOAuthProviderConfigurator.retrieveByOrigin("oidcprovider", "uaa")).thenReturn(idp); + when(identityProviderProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(uaaProvider); + when(identityProviderProvisioning.retrieveByOrigin("ldap", "uaa")).thenReturn(ldapProvider); Authentication clientAuth = mock(Authentication.class); when(clientAuth.getName()).thenReturn("clientid"); @@ -630,22 +635,29 @@ void testUaaPasswordGrant_defaultProviderUaa() { verify(zoneAwareAuthzAuthenticationManager, times(0)).setLoginHint(any(), any()); } - @Test - void testPasswordGrant_NoLoginHintWithDefaultUaa() { + @ParameterizedTest + @ValueSource(strings = { OriginKeys.UAA, OriginKeys.LDAP }) + void testPasswordGrant_NoLoginHintWithDefaultUaaOrLdap(final String loginHintOrigin) { Authentication auth = mock(Authentication.class); when(zoneAwareAuthzAuthenticationManager.extractLoginHint(auth)).thenReturn(null); Map additionalInformation = new HashMap<>(); - additionalInformation.put(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList("uaa")); + additionalInformation.put(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList(loginHintOrigin)); when(uaaClient.getAdditionalInformation()).thenReturn(additionalInformation); - IdentityZoneHolder.get().getConfig().setDefaultIdentityProvider("uaa"); + IdentityZoneHolder.get().getConfig().setDefaultIdentityProvider(loginHintOrigin); instance.authenticate(auth); + /* should read all in the zone during lookup of possible providers + * - "uaa" or "ldap" is used, but not as login hint */ + final String idzId = IdentityZoneHolder.get().getId(); + verify(identityProviderProvisioning, times(1)).retrieveActive(idzId); + verify(identityProviderProvisioning, times(0)).retrieveByOrigin(loginHintOrigin, idzId); + verify(zoneAwareAuthzAuthenticationManager, times(1)).authenticate(auth); ArgumentCaptor captor = ArgumentCaptor.forClass(UaaLoginHint.class); verify(zoneAwareAuthzAuthenticationManager, times(1)).setLoginHint(eq(auth), captor.capture()); - assertNotNull(captor.getValue()); - assertEquals("uaa", captor.getValue().getOrigin()); + Assertions.assertNotNull(captor.getValue()); + Assertions.assertEquals(loginHintOrigin, captor.getValue().getOrigin()); } @Test @@ -722,24 +734,30 @@ void testOIDCPasswordGrant_LoginHintOidcOverridesDefaultUaa() { verify(identityProviderProvisioning, times(0)).retrieveActive(any()); } - @Test - void testOIDCPasswordGrant_LoginHintUaaOverridesDefaultOidc() { + @ParameterizedTest + @ValueSource(strings = { OriginKeys.UAA, OriginKeys.LDAP }) + void testOIDCPasswordGrant_LoginHintUaaOrLdapOverridesDefaultOidc(final String loginHintOrigin) { UaaLoginHint loginHint = mock(UaaLoginHint.class); - when(loginHint.getOrigin()).thenReturn("uaa"); + when(loginHint.getOrigin()).thenReturn(loginHintOrigin); Authentication auth = mock(Authentication.class); - when(zoneAwareAuthzAuthenticationManager.extractLoginHint(auth)).thenReturn(null); + when(zoneAwareAuthzAuthenticationManager.extractLoginHint(auth)).thenReturn(loginHint); Map additionalInformation = new HashMap<>(); - additionalInformation.put(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList("uaa")); + additionalInformation.put(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList(loginHintOrigin)); when(uaaClient.getAdditionalInformation()).thenReturn(additionalInformation); IdentityZoneHolder.get().getConfig().setDefaultIdentityProvider("oidcprovider"); instance.authenticate(auth); + // should read only "uaa" or "ldap" IdP during lookup of possible providers + final String idzId = IdentityZoneHolder.get().getId(); + verify(identityProviderProvisioning, times(0)).retrieveActive(idzId); + verify(identityProviderProvisioning, times(1)).retrieveByOrigin(loginHintOrigin, idzId); + verify(zoneAwareAuthzAuthenticationManager, times(1)).authenticate(auth); ArgumentCaptor captor = ArgumentCaptor.forClass(UaaLoginHint.class); verify(zoneAwareAuthzAuthenticationManager, times(1)).setLoginHint(eq(auth), captor.capture()); - assertNotNull(captor.getValue()); - assertEquals("uaa", captor.getValue().getOrigin()); + Assertions.assertNotNull(captor.getValue()); + Assertions.assertEquals(loginHintOrigin, captor.getValue().getOrigin()); } @Test