diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/Links.java b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/Links.java index 6e9d69d641f..46984bbe24f 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/Links.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/Links.java @@ -17,8 +17,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import java.util.HashSet; -import java.util.Set; +import java.util.Collections; +import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) @@ -59,7 +59,7 @@ public static class Logout { private String redirectUrl = "/login"; private String redirectParameterName = "redirect"; private boolean disableRedirectParameter = false; - private Set whitelist = new HashSet<>(); + private List whitelist = Collections.EMPTY_LIST; public boolean isDisableRedirectParameter() { return disableRedirectParameter; @@ -88,11 +88,11 @@ public Logout setRedirectUrl(String redirectUrl) { return this; } - public Set getWhitelist() { + public List getWhitelist() { return whitelist; } - public Logout setWhitelist(Set whitelist) { + public Logout setWhitelist(List whitelist) { this.whitelist = whitelist; return this; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/WhitelistLogoutHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/WhitelistLogoutHandler.java index 223c9c13c88..3980e9a9ca7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/WhitelistLogoutHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/WhitelistLogoutHandler.java @@ -27,6 +27,20 @@ public WhitelistLogoutHandler(List whitelist) { this.whitelist = whitelist; } + @Override + protected String getTargetUrlParameter() { + return super.getTargetUrlParameter(); + } + + @Override + protected boolean isAlwaysUseDefaultTargetUrl() { + return super.isAlwaysUseDefaultTargetUrl(); + } + + public String getDefaultTargetUrl1() { + return super.getDefaultTargetUrl(); + } + public List getWhitelist() { return whitelist; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandler.java new file mode 100644 index 00000000000..bb148d86340 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandler.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.authentication; + + +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ZoneAwareWhitelistLogoutHandler implements LogoutSuccessHandler { + + private final ClientDetailsService clientDetailsService; + + public ZoneAwareWhitelistLogoutHandler(ClientDetailsService clientDetailsService) { + this.clientDetailsService = clientDetailsService; + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + getZoneHandler().onLogoutSuccess(request, response, authentication); + } + + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { + return getZoneHandler().determineTargetUrl(request, response); + } + + protected WhitelistLogoutHandler getZoneHandler() { + IdentityZoneConfiguration config = IdentityZoneHolder.get().getConfig(); + if (config==null) { + config = new IdentityZoneConfiguration(); + } + WhitelistLogoutHandler handler = new WhitelistLogoutHandler(config.getLinks().getLogout().getWhitelist()); + handler.setTargetUrlParameter(config.getLinks().getLogout().getRedirectParameterName()); + handler.setDefaultTargetUrl(config.getLinks().getLogout().getRedirectUrl()); + handler.setAlwaysUseDefaultTargetUrl(config.getLinks().getLogout().isDisableRedirectParameter()); + handler.setClientDetailsService(clientDetailsService); + return handler; + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java index bab9bd10286..83f23eab4d2 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java @@ -18,8 +18,10 @@ import org.cloudfoundry.identity.uaa.zone.TokenPolicy; import org.springframework.beans.factory.InitializingBean; +import java.util.List; import java.util.Map; +import static java.util.Objects.nonNull; import static org.springframework.util.StringUtils.hasText; public class IdentityZoneConfigurationBootstrap implements InitializingBean { @@ -29,6 +31,10 @@ public class IdentityZoneConfigurationBootstrap implements InitializingBean { private boolean selfServiceLinksEnabled = true; private String homeRedirect = null; private Map selfServiceLinks; + private List logoutRedirectWhitelist; + private String logoutRedirectParameterName; + private String logoutDefaultRedirectUrl; + private boolean logoutDisableRedirectParameter = true; public IdentityZoneConfigurationBootstrap(IdentityZoneProvisioning provisioning) { @@ -51,6 +57,17 @@ public void afterPropertiesSet() { definition.getLinks().getSelfService().setPasswd(passwd); } } + if (nonNull(logoutRedirectWhitelist)) { + definition.getLinks().getLogout().setWhitelist(logoutRedirectWhitelist); + } + if (hasText(logoutRedirectParameterName)) { + definition.getLinks().getLogout().setRedirectParameterName(logoutRedirectParameterName); + } + if (hasText(logoutDefaultRedirectUrl)) { + definition.getLinks().getLogout().setRedirectUrl(logoutDefaultRedirectUrl); + } + definition.getLinks().getLogout().setDisableRedirectParameter(logoutDisableRedirectParameter); + identityZone.setConfig(definition); provisioning.update(identityZone); } @@ -76,4 +93,20 @@ public void setHomeRedirect(String homeRedirect) { public void setSelfServiceLinks(Map links) { this.selfServiceLinks = links; } + + public void setLogoutDefaultRedirectUrl(String logoutDefaultRedirectUrl) { + this.logoutDefaultRedirectUrl = logoutDefaultRedirectUrl; + } + + public void setLogoutDisableRedirectParameter(boolean logoutDisableRedirectParameter) { + this.logoutDisableRedirectParameter = logoutDisableRedirectParameter; + } + + public void setLogoutRedirectParameterName(String logoutRedirectParameterName) { + this.logoutRedirectParameterName = logoutRedirectParameterName; + } + + public void setLogoutRedirectWhitelist(List logoutRedirectWhitelist) { + this.logoutRedirectWhitelist = logoutRedirectWhitelist; + } } diff --git a/server/src/main/resources/login-ui.xml b/server/src/main/resources/login-ui.xml index bdaef0984ed..1fd61b37198 100644 --- a/server/src/main/resources/login-ui.xml +++ b/server/src/main/resources/login-ui.xml @@ -308,15 +308,8 @@ - - - - - - + + diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandlerTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandlerTests.java new file mode 100644 index 00000000000..9d5845b7ed6 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/ZoneAwareWhitelistLogoutHandlerTests.java @@ -0,0 +1,156 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.authentication; + +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.NoSuchClientException; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; + + +public class ZoneAwareWhitelistLogoutHandlerTests { + + private MockHttpServletRequest request = new MockHttpServletRequest(); + private MockHttpServletResponse response = new MockHttpServletResponse(); + private BaseClientDetails client = new BaseClientDetails(CLIENT_ID, "", "", "", "", "http://*.testing.com,http://testing.com"); + private ClientDetailsService clientDetailsService = mock(ClientDetailsService.class); + private ZoneAwareWhitelistLogoutHandler handler; + IdentityZoneConfiguration configuration = new IdentityZoneConfiguration(); + IdentityZoneConfiguration original; + + + @Before + public void setUp() throws Exception { + original = IdentityZone.getUaa().getConfig(); + configuration.getLinks().getLogout() + .setRedirectUrl("/login") + .setDisableRedirectParameter(true) + .setRedirectParameterName("redirect"); + when(clientDetailsService.loadClientByClientId(CLIENT_ID)).thenReturn(client); + handler = new ZoneAwareWhitelistLogoutHandler(clientDetailsService); + IdentityZoneHolder.get().setConfig(configuration); + } + + @After + public void tearDown() throws Exception { + IdentityZoneHolder.clear(); + IdentityZone.getUaa().setConfig(original); + } + + @Test + public void test_defaults() throws Exception { + WhitelistLogoutHandler whandler = handler.getZoneHandler(); + assertEquals(Collections.EMPTY_LIST, whandler.getWhitelist()); + assertEquals("redirect", whandler.getTargetUrlParameter()); + assertEquals("/login", whandler.getDefaultTargetUrl1()); + assertTrue(whandler.isAlwaysUseDefaultTargetUrl()); + } + + @Test + public void test_null_config_defaults() throws Exception { + IdentityZoneHolder.get().setConfig(null); + test_default_redirect_uri(); + } + + + @Test + public void test_default_redirect_uri() throws Exception { + assertEquals("/login", handler.determineTargetUrl(request, response)); + assertEquals("/login", handler.determineTargetUrl(request, response)); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + assertEquals("/login", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_whitelist_reject() throws Exception { + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://testing.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://testing.com"); + assertEquals("http://testing.com", handler.determineTargetUrl(request, response)); + request.setParameter("redirect", "http://www.testing.com"); + assertEquals("/login", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_allow_open_redirect() throws Exception { + configuration.getLinks().getLogout().setWhitelist(null); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://testing.com"); + assertEquals("http://testing.com", handler.determineTargetUrl(request, response)); + request.setParameter("redirect", "http://www.testing.com"); + assertEquals("http://www.testing.com", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_whitelist_redirect() throws Exception { + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://somethingelse.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://somethingelse.com"); + assertEquals("http://somethingelse.com", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_whitelist_redirect_with_wildcard() throws Exception { + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://*.somethingelse.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://www.somethingelse.com"); + assertEquals("http://www.somethingelse.com", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_client_redirect() throws Exception { + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://somethingelse.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://testing.com"); + request.setParameter(CLIENT_ID, CLIENT_ID); + assertEquals("http://testing.com", handler.determineTargetUrl(request, response)); + } + + @Test + public void client_not_found_exception() throws Exception { + when(clientDetailsService.loadClientByClientId("test")).thenThrow(new NoSuchClientException("test")); + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://testing.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter("redirect", "http://notwhitelisted.com"); + request.setParameter(CLIENT_ID, "test"); + assertEquals("/login", handler.determineTargetUrl(request, response)); + } + + @Test + public void test_client_redirect_using_wildcard() throws Exception { + configuration.getLinks().getLogout().setWhitelist(Arrays.asList("http://testing.com")); + configuration.getLinks().getLogout().setDisableRedirectParameter(false); + request.setParameter(CLIENT_ID, CLIENT_ID); + request.setParameter("redirect", "http://www.testing.com"); + assertEquals("http://www.testing.com", handler.determineTargetUrl(request, response)); + } + +} \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java index c56cdc95d8e..5fd500e1af6 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java @@ -23,6 +23,7 @@ import org.junit.Before; import org.junit.Test; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -135,4 +136,18 @@ public void passwd_link_configured() throws Exception { assertEquals("/create_account", zone.getConfig().getLinks().getSelfService().getSignup()); assertEquals("/configured_passwd", zone.getConfig().getLinks().getSelfService().getPasswd()); } + + @Test + public void test_logout_redirect() throws Exception { + bootstrap.setLogoutDefaultRedirectUrl("/configured_login"); + bootstrap.setLogoutDisableRedirectParameter(false); + bootstrap.setLogoutRedirectParameterName("test"); + bootstrap.setLogoutRedirectWhitelist(Arrays.asList("http://single-url")); + bootstrap.afterPropertiesSet(); + IdentityZoneConfiguration config = provisioning.retrieve(IdentityZone.getUaa().getId()).getConfig(); + assertEquals("/configured_login", config.getLinks().getLogout().getRedirectUrl()); + assertEquals("test", config.getLinks().getLogout().getRedirectParameterName()); + assertEquals(Arrays.asList("http://single-url"), config.getLinks().getLogout().getWhitelist()); + assertFalse(config.getLinks().getLogout().isDisableRedirectParameter()); + } } diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index caa7481604a..3d044c37009 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -403,7 +403,17 @@ - + + + + + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index febca7e6734..b7d6d9a88ce 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -163,8 +163,13 @@ public void testRootContextDefaults() throws Exception { assertSame(UaaTokenStore.class, context.getBean(AuthorizationCodeServices.class).getClass()); IdentityZoneProvisioning zoneProvisioning = context.getBean(IdentityZoneProvisioning.class); - assertTrue(zoneProvisioning.retrieve(IdentityZone.getUaa().getId()).getConfig().getLinks().getSelfService().isSelfServiceLinksEnabled()); - assertNull(zoneProvisioning.retrieve(IdentityZone.getUaa().getId()).getConfig().getLinks().getHomeRedirect()); + IdentityZoneConfiguration zoneConfiguration = zoneProvisioning.retrieve(IdentityZone.getUaa().getId()).getConfig(); + assertTrue(zoneConfiguration.getLinks().getSelfService().isSelfServiceLinksEnabled()); + assertNull(zoneConfiguration.getLinks().getHomeRedirect()); + assertEquals("redirect", zoneConfiguration.getLinks().getLogout().getRedirectParameterName()); + assertEquals("/login", zoneConfiguration.getLinks().getLogout().getRedirectUrl()); + assertEquals(Collections.EMPTY_LIST, zoneConfiguration.getLinks().getLogout().getWhitelist()); + assertTrue(zoneConfiguration.getLinks().getLogout().isDisableRedirectParameter()); Object links = context.getBean("links"); assertEquals(Collections.EMPTY_MAP, links); @@ -270,6 +275,12 @@ public void testPropertyValuesWhenSetInYaml() throws Exception { assertEquals("/configured_signup", zoneConfig.getLinks().getSelfService().getSignup()); assertEquals("/configured_passwd", zoneConfig.getLinks().getSelfService().getPasswd()); + assertEquals("redirect", zoneConfig.getLinks().getLogout().getRedirectParameterName()); + assertEquals("/configured_login", zoneConfig.getLinks().getLogout().getRedirectUrl()); + assertEquals(Arrays.asList("https://url1.domain1.com/logout-success","https://url2.domain2.com/logout-success"), zoneConfig.getLinks().getLogout().getWhitelist()); + assertFalse(zoneConfig.getLinks().getLogout().isDisableRedirectParameter()); + + IdentityProviderProvisioning idpProvisioning = context.getBean(IdentityProviderProvisioning.class); IdentityProvider uaaIdp = idpProvisioning.retrieveByOrigin(OriginKeys.UAA, IdentityZone.getUaa().getId()); assertTrue(uaaIdp.getConfig().isDisableInternalUserManagement()); diff --git a/uaa/src/test/resources/test/bootstrap/bootstrap-test.yml b/uaa/src/test/resources/test/bootstrap/bootstrap-test.yml index df81b7aa10a..08e9e0e2130 100644 --- a/uaa/src/test/resources/test/bootstrap/bootstrap-test.yml +++ b/uaa/src/test/resources/test/bootstrap/bootstrap-test.yml @@ -20,6 +20,15 @@ login: selfServiceLinksEnabled: false homeRedirect: http://some.redirect.com/redirect +logout: + redirect: + url: /configured_login + parameter: + disable: false + whitelist: + - https://url1.domain1.com/logout-success + - https://url2.domain2.com/logout-success + links: passwd: /configured_passwd signup: /configured_signup