From 333954df0df9327cea2a75c906a63aa5fea3544e Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Tue, 10 Nov 2020 16:42:22 +0100 Subject: [PATCH 01/76] Restructure login method to not read all IdentityProviders on login_hint --- .../identity/uaa/login/LoginInfoEndpoint.java | 70 ++++++++++++------- .../uaa/login/LoginInfoEndpointTests.java | 24 +++++-- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java index 4a8042215b0..aea0b5682f2 100755 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java @@ -74,6 +74,7 @@ import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -273,15 +274,31 @@ private String login(Model model, Principal principal, List excludedProm clientName = (String) clientInfo.get(ClientConstants.CLIENT_NAME); } - Map samlIdentityProviders = - getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); - Map oauthIdentityProviders = - getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); - Map allIdentityProviders = - new HashMap<>() {{ - putAll(samlIdentityProviders); - putAll(oauthIdentityProviders); - }}; + Map samlIdentityProviders; + Map oauthIdentityProviders; + Map allIdentityProviders = Collections.emptyMap(); + Map loginHintProviders = Collections.emptyMap(); + + String loginHintParam = extractLoginHintParam(session, request); + UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); + if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) { + if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) { + try { + IdentityProvider loginHintProvider = externalOAuthProviderConfigurator + .retrieveByOrigin(uaaLoginHint.getOrigin(), IdentityZoneHolder.get().getId()); + loginHintProviders = Collections.singletonList(loginHintProvider).stream().collect( + new MapCollector( + IdentityProvider::getOriginKey, IdentityProvider::getConfig)); + } catch (EmptyResultDataAccessException ignored) { + } + } + oauthIdentityProviders = Collections.emptyMap(); + samlIdentityProviders = Collections.emptyMap(); + } else { + samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); + oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); + allIdentityProviders = new HashMap<>() {{putAll(samlIdentityProviders);putAll(oauthIdentityProviders);}}; + } boolean fieldUsernameShow = true; boolean returnLoginPrompts = true; @@ -311,8 +328,8 @@ private String login(Model model, Principal principal, List excludedProm } Map.Entry idpForRedirect; - idpForRedirect = evaluateLoginHint(model, session, samlIdentityProviders, - oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, request); + idpForRedirect = evaluateLoginHint(model, samlIdentityProviders, + oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders); boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled(); boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed")); @@ -480,27 +497,29 @@ private Map.Entry evaluateIdpDiscove return idpForRedirect; } + private String extractLoginHintParam(HttpSession session, HttpServletRequest request) { + String loginHintParam = + ofNullable(session) + .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s))) + .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint"))) + .flatMap(lhValues -> Arrays.stream(lhValues).findFirst()) + .orElse(request.getParameter("login_hint")); + return loginHintParam; + } + private Map.Entry evaluateLoginHint( Model model, - HttpSession session, Map samlIdentityProviders, Map oauthIdentityProviders, Map allIdentityProviders, List allowedIdentityProviderKeys, - HttpServletRequest request + String loginHintParam, + UaaLoginHint uaaLoginHint, + Map loginHintProviders ) { - Map.Entry idpForRedirect = null; - String loginHintParam = - ofNullable(session) - .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s))) - .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint"))) - .flatMap(lhValues -> Arrays.stream(lhValues).findFirst()) - .orElse(request.getParameter("login_hint")); - if (loginHintParam != null) { // parse login_hint in JSON format - UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); if (uaaLoginHint != null) { logger.debug("Received login hint: " + loginHintParam); logger.debug("Received login hint with origin: " + uaaLoginHint.getOrigin()); @@ -519,12 +538,13 @@ private Map.Entry evaluateLoginHint( allIdentityProviders.entrySet().stream().filter( idp -> idp.getKey().equals(uaaLoginHint.getOrigin()) ).collect(Collectors.toList()); - if (hintIdentityProviders.size() > 1) { + if (loginHintProviders.size() > 1) { throw new IllegalStateException( "There is a misconfiguration with the identity provider(s). Please contact your system administrator." ); - } else if (hintIdentityProviders.size() == 1) { - idpForRedirect = hintIdentityProviders.get(0); + } + if (loginHintProviders.size() == 1) { + idpForRedirect = new ArrayList<>(loginHintProviders.entrySet()).get(0); logger.debug("Setting redirect from origin login_hint to: " + idpForRedirect); } else { logger.debug("Client does not allow provider for login_hint with origin key: " diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java index 9e1dbb0d570..9f6f0110237 100755 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -1163,7 +1164,7 @@ void loginHintOriginOidc() throws Exception { MultitenantClientServices clientDetailsService = mockClientService(); - mockOidcProvider(mockIdentityProviderProvisioning); + mockLoginHintProvider(configurator); LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService); @@ -1184,7 +1185,7 @@ void loginHintOriginOidcForJson() throws Exception { MultitenantClientServices clientDetailsService = mockClientService(); - mockOidcProvider(mockIdentityProviderProvisioning); + mockLoginHintProvider(configurator); LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService); @@ -1197,7 +1198,7 @@ void loginHintOriginOidcForJson() throws Exception { assertNotNull(extendedModelMap.get("prompts")); assertTrue(extendedModelMap.get("prompts") instanceof Map); Map returnedPrompts = (Map) extendedModelMap.get("prompts"); - assertEquals(3, returnedPrompts.size()); + assertEquals(2, returnedPrompts.size()); } @Test @@ -1210,6 +1211,7 @@ void loginHintOriginInvalid() throws Exception { SavedRequest savedRequest = SessionUtils.getSavedRequestSession(mockHttpServletRequest.getSession()); when(savedRequest.getParameterValues("login_hint")).thenReturn(new String[]{"{\"origin\":\"my-OIDC-idp1\"}"}); + when(configurator.retrieveByOrigin(eq("my-OIDC-idp1"), anyString())).thenThrow(new EmptyResultDataAccessException(0)); endpoint.loginForHtml(extendedModelMap, null, mockHttpServletRequest, singletonList(MediaType.TEXT_HTML)); @@ -1405,7 +1407,7 @@ void loginHintOverridesDefaultProvider() throws Exception { MultitenantClientServices clientDetailsService = mockClientService(); - mockOidcProvider(mockIdentityProviderProvisioning); + mockLoginHintProvider(configurator); LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService); @@ -1656,4 +1658,18 @@ private static void mockOidcProvider(IdentityProviderProvisioning mockIdentityPr when(mockOidcConfig.isShowLinkText()).thenReturn(true); when(mockIdentityProviderProvisioning.retrieveAll(anyBoolean(), any())).thenReturn(singletonList(mockProvider)); } + + private static void mockLoginHintProvider(ExternalOAuthProviderConfigurator mockIdentityProviderProvisioning) + throws MalformedURLException { + IdentityProvider mockProvider = mock(IdentityProvider.class); + when(mockProvider.getOriginKey()).thenReturn("my-OIDC-idp1"); + when(mockProvider.getType()).thenReturn(OriginKeys.OIDC10); + AbstractExternalOAuthIdentityProviderDefinition mockOidcConfig = mock(OIDCIdentityProviderDefinition.class); + when(mockOidcConfig.getAuthUrl()).thenReturn(new URL("http://localhost:8080/uaa")); + when(mockOidcConfig.getRelyingPartyId()).thenReturn("client-id"); + when(mockOidcConfig.getResponseType()).thenReturn("token"); + when(mockProvider.getConfig()).thenReturn(mockOidcConfig); + when(mockOidcConfig.isShowLinkText()).thenReturn(true); + when(mockIdentityProviderProvisioning.retrieveByOrigin(eq("my-OIDC-idp1"), any())).thenReturn(mockProvider); + } } From 0e31bb4cce3399d242b477dd2a9d992e2bcfc407 Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Thu, 12 Nov 2020 11:29:25 +0100 Subject: [PATCH 02/76] Move read configurations and parameters to allow earlier decisions --- .../identity/uaa/login/LoginInfoEndpoint.java | 36 ++++++++++--------- .../uaa/login/LoginInfoEndpointTests.java | 7 ---- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java index aea0b5682f2..d9b3c8a276e 100755 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java @@ -274,14 +274,25 @@ private String login(Model model, Principal principal, List excludedProm clientName = (String) clientInfo.get(ClientConstants.CLIENT_NAME); } + //Read all configuration and parameters at the beginning to allow earlier decisions + boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled(); + boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed")); + String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider(); + boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled(); + boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn")); + boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty(); + boolean accountChooserNeeded = accountChooserEnabled && !(otherAccountSignIn || savedAccountsEmpty) && discoveryEnabled &&!discoveryPerformed; + + String loginHintParam = extractLoginHintParam(session, request); + UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); + Map samlIdentityProviders; Map oauthIdentityProviders; Map allIdentityProviders = Collections.emptyMap(); Map loginHintProviders = Collections.emptyMap(); - String loginHintParam = extractLoginHintParam(session, request); - UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) { + // Login hint: Only try to read the hinted IdP from database if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) { try { IdentityProvider loginHintProvider = externalOAuthProviderConfigurator @@ -294,6 +305,10 @@ private String login(Model model, Principal principal, List excludedProm } oauthIdentityProviders = Collections.emptyMap(); samlIdentityProviders = Collections.emptyMap(); + } else if (accountChooserNeeded || (discoveryEnabled && !discoveryPerformed)) { + //Account Chooser and discovery do not need any IdP information + oauthIdentityProviders = Collections.emptyMap(); + samlIdentityProviders = Collections.emptyMap(); } else { samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); @@ -331,10 +346,6 @@ private String login(Model model, Principal principal, List excludedProm idpForRedirect = evaluateLoginHint(model, samlIdentityProviders, oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders); - boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled(); - boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed")); - String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider(); - idpForRedirect = evaluateIdpDiscovery(model, samlIdentityProviders, oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, idpForRedirect, discoveryEnabled, discoveryPerformed, defaultIdentityProviderName); if (idpForRedirect == null && !jsonResponse && !fieldUsernameShow && allIdentityProviders.size() == 1) { @@ -381,7 +392,7 @@ private String login(Model model, Principal principal, List excludedProm excludedPrompts, returnLoginPrompts); if (principal == null) { - return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed); + return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed, accountChooserNeeded); } return "home"; } @@ -390,25 +401,18 @@ private String getUnauthenticatedRedirect( Model model, HttpServletRequest request, boolean discoveryEnabled, - boolean discoveryPerformed + boolean discoveryPerformed, + boolean accountChooserNeeded ) { String formRedirectUri = request.getParameter(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER); if (hasText(formRedirectUri)) { model.addAttribute(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER, formRedirectUri); } - boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled(); - boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn")); - boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty(); - if (discoveryEnabled) { if (model.containsAttribute("login_hint")) { return goToPasswordPage(request.getParameter("email"), model); } - boolean accountChooserNeeded = accountChooserEnabled - && !(otherAccountSignIn || savedAccountsEmpty) - && !discoveryPerformed; - if (accountChooserNeeded) { return "idp_discovery/account_chooser"; } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java index 9f6f0110237..d498b30de69 100755 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java @@ -818,13 +818,6 @@ void authcodeWithAllowedProviderStillUsesAccountChooser() throws Exception { MultitenantClientServices clientDetailsService = mock(MultitenantClientServices.class); when(clientDetailsService.loadClientByClientId("client-id", "other-zone")).thenReturn(clientDetails); - // mock SamlIdentityProviderConfigurator - List clientIDPs = new LinkedList<>(); - clientIDPs.add(createIdentityProviderDefinition("my-client-awesome-idp1", "other-zone")); - clientIDPs.add(createIdentityProviderDefinition("uaa", "other-zone")); - when(mockSamlIdentityProviderConfigurator.getIdentityProviderDefinitions(eq(allowedProviders), eq(zone))).thenReturn(clientIDPs); - - LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService); endpoint.loginForHtml(extendedModelMap, null, request, singletonList(MediaType.TEXT_HTML)); From 7e7c376cd4bc5180eb78b43bcc33b21b069bb8aa Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Fri, 29 Jan 2021 13:18:49 +0100 Subject: [PATCH 03/76] Fix bug with invalid login_hint on login page --- .../identity/uaa/login/LoginInfoEndpoint.java | 12 ++++++++-- .../uaa/login/LoginInfoEndpointTests.java | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java index d9b3c8a276e..9776b363adf 100755 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java @@ -303,8 +303,16 @@ private String login(Model model, Principal principal, List excludedProm } catch (EmptyResultDataAccessException ignored) { } } - oauthIdentityProviders = Collections.emptyMap(); - samlIdentityProviders = Collections.emptyMap(); + if (!loginHintProviders.isEmpty()) { + oauthIdentityProviders = Collections.emptyMap(); + samlIdentityProviders = Collections.emptyMap(); + } else { + samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); + oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); + allIdentityProviders = new HashMap<>(); + allIdentityProviders.putAll(samlIdentityProviders); + allIdentityProviders.putAll(oauthIdentityProviders); + } } else if (accountChooserNeeded || (discoveryEnabled && !discoveryPerformed)) { //Account Chooser and discovery do not need any IdP information oauthIdentityProviders = Collections.emptyMap(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java index d498b30de69..e410ed4aa9e 100755 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java @@ -1151,6 +1151,30 @@ void invalidLoginHintErrorOnDiscoveryPage() throws Exception { assertEquals("idp_discovery/email", redirect); } + @Test + public void testInvalidLoginHintLoginPageReturnsList() throws Exception { + MockHttpServletRequest mockHttpServletRequest = getMockHttpServletRequest(); + + BaseClientDetails clientDetails = new BaseClientDetails(); + clientDetails.setClientId("client-id"); + MultitenantClientServices clientDetailsService = mock(MultitenantClientServices.class); + when(clientDetailsService.loadClientByClientId("client-id", "uaa")).thenReturn(clientDetails); + LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService); + + List clientAllowedIdps = new LinkedList<>(); + clientAllowedIdps.add(createOIDCIdentityProvider("my-OIDC-idp1")); + clientAllowedIdps.add(createOIDCIdentityProvider("my-OIDC-idp2")); + when(mockIdentityProviderProvisioning.retrieveAll(eq(true), anyString())).thenReturn(clientAllowedIdps); + when(mockIdentityProviderProvisioning.retrieveByOrigin(eq("invalidorigin"), anyString())).thenThrow(new EmptyResultDataAccessException(1)); + + SavedRequest savedRequest = SessionUtils.getSavedRequestSession(mockHttpServletRequest.getSession()); + when(savedRequest.getParameterValues("login_hint")).thenReturn(new String[]{"{\"origin\":\"invalidorigin\"}"}); + + endpoint.loginForHtml(extendedModelMap, null, mockHttpServletRequest, Collections.singletonList(MediaType.TEXT_HTML)); + + assertFalse(((Map)extendedModelMap.get("oauthLinks")).isEmpty()); + } + @Test void loginHintOriginOidc() throws Exception { MockHttpServletRequest mockHttpServletRequest = getMockHttpServletRequest(); From c32c34908d7e885202ef6ea86a143732c16cb121 Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Fri, 29 Jan 2021 13:19:10 +0100 Subject: [PATCH 04/76] Add MockMvc Test to show performance improvement --- .../LoginPagePerformanceMockMvcTest.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java new file mode 100644 index 00000000000..3ad5ab09761 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java @@ -0,0 +1,166 @@ +package org.cloudfoundry.identity.uaa.performance; + +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createOtherIdentityZoneAndReturnResult; +import static org.junit.Assert.assertFalse; +import static org.springframework.http.MediaType.TEXT_HTML; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.File; +import java.net.URL; +import java.util.Collections; + +import org.cloudfoundry.identity.uaa.DefaultTestContext; +import org.cloudfoundry.identity.uaa.codestore.JdbcExpiringCodeStore; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.impl.config.IdentityZoneConfigurationBootstrap; +import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.oauth.OidcMetadataFetcher; +import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; +import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StopWatch; +import org.springframework.util.StringUtils; +import org.springframework.web.context.WebApplicationContext; + +@DefaultTestContext +@DirtiesContext +public class LoginPagePerformanceMockMvcTest { + + private WebApplicationContext webApplicationContext; + + private RandomValueStringGenerator generator; + + private MockMvc mockMvc; + + + + private File originalLimitedModeStatusFile; + + @MockBean + OidcMetadataFetcher oidcMetadataFetcher; + + @BeforeEach + void setUpContext( + @Autowired WebApplicationContext webApplicationContext, + @Autowired MockMvc mockMvc, + @Autowired LimitedModeUaaFilter limitedModeUaaFilter + ) { + generator = new RandomValueStringGenerator(); + this.webApplicationContext = webApplicationContext; + this.mockMvc = mockMvc; + SecurityContextHolder.clearContext(); + + originalLimitedModeStatusFile = MockMvcUtils.getLimitedModeStatusFile(webApplicationContext); + MockMvcUtils.resetLimitedModeStatusFile(webApplicationContext, null); + assertFalse(limitedModeUaaFilter.isEnabled()); + } + + @AfterEach + void resetGenerator( + @Autowired JdbcExpiringCodeStore jdbcExpiringCodeStore + ) { + jdbcExpiringCodeStore.setGenerator(new RandomValueStringGenerator(24)); + } + + @AfterEach + void tearDown(@Autowired IdentityZoneConfigurationBootstrap identityZoneConfigurationBootstrap) throws Exception { + MockMvcUtils.setSelfServiceLinksEnabled(webApplicationContext, IdentityZone.getUaaZoneId(), true); + identityZoneConfigurationBootstrap.afterPropertiesSet(); + SecurityContextHolder.clearContext(); + IdentityZoneHolder.clear(); + MockMvcUtils.resetLimitedModeStatusFile(webApplicationContext, originalLimitedModeStatusFile); + } + + @Test + void idpDiscoveryRedirectsToOIDCProvider( + @Autowired JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning + ) throws Exception { + String subdomain = "oidc-discovery-" + generator.generate().toLowerCase(); + IdentityZone zone = MultitenancyFixture.identityZone(subdomain, subdomain); + zone.getConfig().setIdpDiscoveryEnabled(true); + BaseClientDetails client = new BaseClientDetails("admin", null, null, "client_credentials", + "clients.admin,scim.read,scim.write,idps.write,uaa.admin", "http://redirect.url"); + client.setClientSecret("admin-secret"); + createOtherIdentityZoneAndReturnResult(mockMvc, webApplicationContext, client, zone, false, IdentityZoneHolder.getCurrentZoneId()); + + + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null); + String originKey = createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", "test.org"); + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + for (int i = 0; i <1000; i++) { + MvcResult mvcResult = mockMvc.perform(get("/login") + .with(cookieCsrf()) + .header("Accept", TEXT_HTML) + .with(new SetServerNameRequestPostProcessor(zone.getSubdomain() + ".localhost"))) + .andExpect(status().isOk()) + .andReturn(); + MockHttpServletResponse response = mvcResult.getResponse(); + } + + stopWatch.stop(); + long totalTimeMillis = stopWatch.getTotalTimeMillis(); + + System.out.println(totalTimeMillis + "ms"); + } + + + private static String createOIDCProvider(JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning, RandomValueStringGenerator generator, IdentityZone zone, String responseType, String domain) throws Exception { + String originKey = generator.generate(); + AbstractExternalOAuthIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition(); + definition.setAuthUrl(new URL("http://myauthurl.com")); + definition.setTokenKey("key"); + definition.setTokenUrl(new URL("http://mytokenurl.com")); + definition.setRelyingPartyId("id"); + definition.setRelyingPartySecret("secret"); + definition.setLinkText("my oidc provider"); + if (StringUtils.hasText(responseType)) { + definition.setResponseType(responseType); + } + if (StringUtils.hasText(domain)) { + definition.setEmailDomain(Collections.singletonList(domain)); + } + + IdentityProvider identityProvider = MultitenancyFixture.identityProvider(originKey, zone.getId()); + identityProvider.setType(OriginKeys.OIDC10); + identityProvider.setConfig(definition); + createIdentityProvider(jdbcIdentityProviderProvisioning, zone, identityProvider); + return originKey; + } + + private static IdentityProvider createIdentityProvider(JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning, IdentityZone identityZone, IdentityProvider activeIdentityProvider) { + activeIdentityProvider.setIdentityZoneId(identityZone.getId()); + return jdbcIdentityProviderProvisioning.create(activeIdentityProvider, identityZone.getId()); + } +} From e21ece5dc99c129c7616ab0322c4c803c1b18764 Mon Sep 17 00:00:00 2001 From: Cloud Foundry Identity Team Date: Wed, 3 Feb 2021 00:53:19 +0000 Subject: [PATCH 05/76] Update UAA image reference in k8s deployment template to cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c --- k8s/templates/values/image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml index 54e35b92c5b..0ac73de2027 100644 --- a/k8s/templates/values/image.yml +++ b/k8s/templates/values/image.yml @@ -1,3 +1,3 @@ #@data/values --- -image: "cloudfoundry/uaa@sha256:37fcf63a156b75174fbc7be0e3809d64cda6851de0bfe6fa7f12793d919de96a" +image: "cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c" From ecf3734d0b8533f1571220c450979f5595d92edf Mon Sep 17 00:00:00 2001 From: Bruce Ricard Date: Thu, 4 Feb 2021 11:51:58 -0800 Subject: [PATCH 06/76] Bump dependency * the previous version of mime has a vulnerability https://nvd.nist.gov/vuln/detail/CVE-2017-16138 --- uaa/slate/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/slate/package.json b/uaa/slate/package.json index db4427c6ca5..b11f2920864 100644 --- a/uaa/slate/package.json +++ b/uaa/slate/package.json @@ -116,7 +116,7 @@ "map-obj": "^1.0.1", "media-typer": "^0.3.0", "meow": "^3.7.0", - "mime": "^1.3.4", + "mime": "^2.5.0", "mime-db": "^1.22.0", "mime-types": "^2.1.10", "minimatch": "^3.0.0", From 9834fc3405b31a0a887d22e48e4162d57364217c Mon Sep 17 00:00:00 2001 From: Cloud Foundry Identity Team Date: Sun, 14 Feb 2021 06:41:15 +0000 Subject: [PATCH 07/76] Update UAA image reference in k8s deployment template to cloudfoundry/uaa@sha256:7fd48f08134e279a4fe2b8a200ecfdca8cda847175df38f1293e9818d7dd53cc --- k8s/templates/values/image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml index 0ac73de2027..0a6418f398e 100644 --- a/k8s/templates/values/image.yml +++ b/k8s/templates/values/image.yml @@ -1,3 +1,3 @@ #@data/values --- -image: "cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c" +image: "cloudfoundry/uaa@sha256:7fd48f08134e279a4fe2b8a200ecfdca8cda847175df38f1293e9818d7dd53cc" From fdd4e72ee103ba12f9baef19058eb0acc9edaa53 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Thu, 25 Feb 2021 11:55:01 -0800 Subject: [PATCH 08/76] feat: for unit tests, add time out to waiting for DB to start - and print out the DB boot log if fail to start [#177100664] --- scripts/start_db_helper.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/start_db_helper.sh b/scripts/start_db_helper.sh index b7dcb37889a..036e9c9f3d8 100755 --- a/scripts/start_db_helper.sh +++ b/scripts/start_db_helper.sh @@ -16,7 +16,8 @@ function bootDB { db=$1 if [[ "${db}" = "postgresql" ]]; then - launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> /var/log/postgres-boot.log) &" + bootLogLocation="/var/log/postgres-boot.log" + launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> ${bootLogLocation}) &" testConnection="(! ps aux | grep docker-entrypoint | grep -v 'grep') && psql -h localhost -U postgres -c '\conninfo' &>/dev/null" initDB="psql -c 'drop database if exists uaa;' -U postgres; psql -c 'create database uaa;' -U postgres; psql -c 'drop user if exists root;' --dbname=uaa -U postgres; psql -c \"create user root with superuser password 'changeme';\" --dbname=uaa -U postgres; psql -c 'show max_connections;' --dbname=uaa -U postgres;" @@ -27,7 +28,8 @@ function bootDB { elif [[ "${db}" = "mysql" ]] || [[ "${db}" = "mysql-5.6" ]]; then - launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> /var/log/mysql-boot.log) &" + bootLogLocation="/var/log/mysql-boot.log" + launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> ${bootLogLocation}) &" testConnection="echo '\s;' | mysql -uroot -pchangeme &>/dev/null" initDB="mysql -uroot -pchangeme -e 'SET GLOBAL max_connections = 250; ALTER DATABASE uaa DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;';" @@ -37,7 +39,8 @@ function bootDB { } elif [[ "${db}" = "percona" ]]; then - launchDB="bash /entrypoint.sh &> /var/log/mysql-boot.log" + bootLogLocation="/var/log/mysql-boot.log" + launchDB="bash /entrypoint.sh &> ${bootLogLocation}" testConnection="echo '\s;' | mysql &>/dev/null" initDB="mysql -e \"CREATE USER 'root'@'127.0.0.1' IDENTIFIED BY 'changeme' ;\"; mysql -e \"GRANT ALL ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION ;\"; @@ -60,7 +63,9 @@ function bootDB { echo -n "Booting $db" set -x eval "$launchDB" - while true; do + + for i in {0..600} # wait at most 10 mins to the database to start + do set +ex eval "$testConnection" exitcode=$? @@ -80,4 +85,8 @@ function bootDB { echo -n "." sleep 1 done + + echo "Printing database boot logs:" + cat "$bootLogLocation" + exit 1 } From 766e981735b144aa33b674b7f3fab9d798a8f3d0 Mon Sep 17 00:00:00 2001 From: Olivier Lechevalier Date: Mon, 26 Oct 2020 17:07:48 +0900 Subject: [PATCH 09/76] Fix typo --- .../manager/DynamicZoneAwareAuthenticationManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java index fc0ffc45b32..1eb215e7691 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java @@ -142,7 +142,7 @@ void testNonUAAZoneUaaActiveAccountLocked() { } @Test - void testNonUAAZoneUaaActiveUaaAuthenticationSucccess() { + void testNonUAAZoneUaaActiveUaaAuthenticationSuccess() { IdentityZoneHolder.set(ZONE); when(providerProvisioning.retrieveByOrigin(OriginKeys.UAA, ZONE.getId())).thenReturn(uaaActive); when(providerProvisioning.retrieveByOrigin(OriginKeys.LDAP, ZONE.getId())).thenReturn(ldapActive); From c88dfff40118d395b71607b4d3a2e265168625e4 Mon Sep 17 00:00:00 2001 From: Markus Strehle Date: Fri, 19 Mar 2021 09:28:58 +0100 Subject: [PATCH 10/76] refactor: implement recommendation from thymeleaf (#1512) by change I found warnings in log. in total 3 warnings found [THYMELEAF][Test worker] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead. [THYMELEAF][Test worker] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead. Initializing Spring TestDispatcherServlet '' Initializing Servlet '' Completed initialization in 3 ms The layout:decorator/data-layout-decorator processor has been deprecated and will be removed in the next major version of the layout dialect. Please use layout:decorate/data-layout-decorate instead to future-proof your code. See https://github.com/ultraq/thymeleaf-layout-dialect/issues/95 for more information. Fragment expression "layouts/main" is being wrapped as a Thymeleaf 3 fragment expression (~{...}) for backwards compatibility purposes. This wrapping will be dropped in the next major version of the expression processor, so please rewrite as a Thymeleaf 3 fragment expression to future-proof your code. See https://github.com/thymeleaf/thymeleaf/issues/451 for more information. ## https://github.com/ultraq/thymeleaf-layout-dialect/issues/95 , changed layout:decorator to layout:decorate ## use template mode HTML instead of HTML5 ## swtiched back to from th:with="isLdap which was removed with spring update --- .../identity/uaa/login/ThymeleafConfig.java | 3 ++- .../resources/templates/web/access_confirmation.html | 2 +- .../templates/web/access_confirmation_error.html | 2 +- .../resources/templates/web/accounts/email_sent.html | 2 +- .../resources/templates/web/accounts/link_prompt.html | 2 +- .../templates/web/accounts/new_activation_email.html | 2 +- server/src/main/resources/templates/web/approvals.html | 2 +- .../src/main/resources/templates/web/change_email.html | 2 +- .../main/resources/templates/web/change_password.html | 2 +- .../src/main/resources/templates/web/email_sent.html | 2 +- server/src/main/resources/templates/web/error.html | 2 +- .../resources/templates/web/external_auth_error.html | 2 +- .../resources/templates/web/force_password_change.html | 2 +- .../main/resources/templates/web/forgot_password.html | 2 +- server/src/main/resources/templates/web/home.html | 2 +- .../templates/web/idp_discovery/account_chooser.html | 2 +- .../resources/templates/web/idp_discovery/email.html | 2 +- .../templates/web/idp_discovery/password.html | 2 +- .../main/resources/templates/web/invalid_request.html | 2 +- .../templates/web/invitations/accept_invite.html | 10 +++++----- server/src/main/resources/templates/web/login.html | 2 +- .../main/resources/templates/web/login_implicit.html | 2 +- .../main/resources/templates/web/mfa/enter_code.html | 2 +- .../templates/web/mfa/manual_registration.html | 2 +- .../src/main/resources/templates/web/mfa/qr_code.html | 2 +- server/src/main/resources/templates/web/passcode.html | 2 +- .../main/resources/templates/web/reset_password.html | 2 +- .../src/main/resources/templates/web/switch_idp.html | 2 +- 28 files changed, 33 insertions(+), 32 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java index 673230e6472..7efd81c0e43 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java @@ -28,6 +28,7 @@ import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.view.ThymeleafViewResolver; +import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ITemplateResolver; import java.nio.charset.StandardCharsets; @@ -104,7 +105,7 @@ public org.springframework.web.servlet.view.ContentNegotiatingViewResolver viewR private SpringResourceTemplateResolver baseHtmlTemplateResolver(ApplicationContext context) { SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setSuffix(".html"); - templateResolver.setTemplateMode("HTML5"); + templateResolver.setTemplateMode(TemplateMode.HTML); templateResolver.setApplicationContext(context); return templateResolver; } diff --git a/server/src/main/resources/templates/web/access_confirmation.html b/server/src/main/resources/templates/web/access_confirmation.html index 99e230ec0ba..a66b703a9e3 100644 --- a/server/src/main/resources/templates/web/access_confirmation.html +++ b/server/src/main/resources/templates/web/access_confirmation.html @@ -2,7 +2,7 @@ + layout:decorate="~{layouts/main}"> diff --git a/server/src/main/resources/templates/web/access_confirmation_error.html b/server/src/main/resources/templates/web/access_confirmation_error.html index 7d454fd650c..2fbe2f85337 100644 --- a/server/src/main/resources/templates/web/access_confirmation_error.html +++ b/server/src/main/resources/templates/web/access_confirmation_error.html @@ -1,5 +1,5 @@ - +
diff --git a/server/src/main/resources/templates/web/accounts/email_sent.html b/server/src/main/resources/templates/web/accounts/email_sent.html index 5b0d5084c10..389d37b61b0 100644 --- a/server/src/main/resources/templates/web/accounts/email_sent.html +++ b/server/src/main/resources/templates/web/accounts/email_sent.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}"> diff --git a/server/src/main/resources/templates/web/accounts/link_prompt.html b/server/src/main/resources/templates/web/accounts/link_prompt.html index 145b18d69fa..27d34953a52 100644 --- a/server/src/main/resources/templates/web/accounts/link_prompt.html +++ b/server/src/main/resources/templates/web/accounts/link_prompt.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}">

Create your account

diff --git a/server/src/main/resources/templates/web/accounts/new_activation_email.html b/server/src/main/resources/templates/web/accounts/new_activation_email.html index a77bc756f2b..7150431ff98 100644 --- a/server/src/main/resources/templates/web/accounts/new_activation_email.html +++ b/server/src/main/resources/templates/web/accounts/new_activation_email.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}">

Create your account

diff --git a/server/src/main/resources/templates/web/approvals.html b/server/src/main/resources/templates/web/approvals.html index a72c4348b7d..6d392a12450 100644 --- a/server/src/main/resources/templates/web/approvals.html +++ b/server/src/main/resources/templates/web/approvals.html @@ -2,7 +2,7 @@ + layout:decorate="~{layouts/main}">