http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java deleted file mode 100644 index e53e701..0000000 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.cloudstack.api.command; - -import com.cloud.utils.HttpUtils; -import org.apache.cloudstack.api.ApiServerService; -import org.apache.cloudstack.api.auth.APIAuthenticationType; -import org.apache.cloudstack.saml.SAML2AuthManager; -import org.apache.cloudstack.utils.auth.SAMLUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.lang.reflect.Field; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.net.InetAddress; -import java.net.UnknownHostException; - -@RunWith(MockitoJUnitRunner.class) -public class GetServiceProviderMetaDataCmdTest { - - @Mock - ApiServerService apiServer; - - @Mock - SAML2AuthManager samlAuthManager; - - @Mock - HttpSession session; - - @Mock - HttpServletResponse resp; - - @Mock - HttpServletRequest req; - - @Test - public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException, UnknownHostException { - GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd(); - - Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer"); - apiServerField.setAccessible(true); - apiServerField.set(cmd, apiServer); - - Field managerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_samlAuthManager"); - managerField.setAccessible(true); - managerField.set(cmd, samlAuthManager); - - String spId = "someSPID"; - String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); - Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); - Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); - Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); - Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url); - - String result = cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); - Assert.assertTrue(result.contains("md:EntityDescriptor")); - - Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getServiceProviderId(); - Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleSignOnUrl(); - Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleLogOutUrl(); - Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleSignOnUrl(); - Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleLogOutUrl(); - } - - @Test - public void testGetAPIType() { - Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API); - } -}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 8fbed41..6960b3b 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -29,9 +29,10 @@ import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.auth.APIAuthenticationType; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.saml.SAML2AuthManager; -import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.apache.cloudstack.saml.SAMLPluginConstants; +import org.apache.cloudstack.saml.SAMLProviderMetadata; +import org.apache.cloudstack.saml.SAMLUtils; import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Test; @@ -43,6 +44,7 @@ import org.opensaml.common.SAMLVersion; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.AuthnStatement; +import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.Response; @@ -52,6 +54,7 @@ import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.impl.AssertionBuilder; import org.opensaml.saml2.core.impl.AttributeStatementBuilder; import org.opensaml.saml2.core.impl.AuthnStatementBuilder; +import org.opensaml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml2.core.impl.NameIDBuilder; import org.opensaml.saml2.core.impl.ResponseBuilder; import org.opensaml.saml2.core.impl.StatusBuilder; @@ -62,6 +65,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.lang.reflect.Field; +import java.security.KeyPair; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; @@ -77,9 +81,6 @@ public class SAML2LoginAPIAuthenticatorCmdTest { SAML2AuthManager samlAuthManager; @Mock - ConfigurationDao configDao; - - @Mock DomainManager domainMgr; @Mock @@ -105,6 +106,9 @@ public class SAML2LoginAPIAuthenticatorCmdTest { samlMessage.setID("foo"); samlMessage.setVersion(SAMLVersion.VERSION_20); samlMessage.setIssueInstant(new DateTime(0)); + Issuer issuer = new IssuerBuilder().buildObject(); + issuer.setValue("MockedIssuer"); + samlMessage.setIssuer(issuer); Status status = new StatusBuilder().buildObject(); StatusCode statusCode = new StatusCodeBuilder().buildObject(); statusCode.setValue(StatusCode.SUCCESS_URI); @@ -146,32 +150,33 @@ public class SAML2LoginAPIAuthenticatorCmdTest { domainMgrField.setAccessible(true); domainMgrField.set(cmd, domainMgr); - Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao"); - configDaoField.setAccessible(true); - configDaoField.set(cmd, configDao); - Field userAccountDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_userAccountDao"); userAccountDaoField.setAccessible(true); userAccountDaoField.set(cmd, userAccountDao); String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); - Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); - Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null); - Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url); - Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url); + KeyPair kp = SAMLUtils.generateRandomKeyPair(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); + + SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); + providerMetadata.setEntityId("random"); + providerMetadata.setSigningCertificate(cert); + providerMetadata.setEncryptionCertificate(cert); + providerMetadata.setKeyPair(kp); + providerMetadata.setSsoUrl("http://test.local"); + providerMetadata.setSloUrl("http://test.local"); Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); - Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString"); Mockito.when(domain.getId()).thenReturn(1L); Mockito.when(domainMgr.getDomain(Mockito.anyString())).thenReturn(domain); UserAccountVO user = new UserAccountVO(); - user.setUsername(SAMLUtils.createSAMLId("someUID")); user.setId(1000L); Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(user); Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false); + Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata); + Mockito.when(samlAuthManager.getIdPMetadata(Mockito.anyString())).thenReturn(providerMetadata); Map<String, Object[]> params = new HashMap<String, Object[]>(); @@ -180,16 +185,14 @@ public class SAML2LoginAPIAuthenticatorCmdTest { Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString()); // SSO SAMLResponse verification test, this should throw ServerApiException for auth failure - params.put(SAMLUtils.SAML_RESPONSE, new String[]{"Some String"}); + params.put(SAMLPluginConstants.SAML_RESPONSE, new String[]{"Some String"}); Mockito.stub(cmd.processSAMLResponse(Mockito.anyString())).toReturn(buildMockResponse()); try { cmd.authenticate("command", params, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); } catch (ServerApiException ignored) { } - Mockito.verify(configDao, Mockito.atLeastOnce()).getValue(Mockito.anyString()); - Mockito.verify(domainMgr, Mockito.times(1)).getDomain(Mockito.anyString()); - Mockito.verify(userAccountDao, Mockito.times(1)).getUserAccount(Mockito.anyString(), Mockito.anyLong()); - Mockito.verify(apiServer, Mockito.times(1)).verifyUser(Mockito.anyLong()); + Mockito.verify(userAccountDao, Mockito.times(0)).getUserAccount(Mockito.anyString(), Mockito.anyLong()); + Mockito.verify(apiServer, Mockito.times(0)).verifyUser(Mockito.anyLong()); } @Test http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java index 4388b88..cbfcc55 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -22,9 +22,8 @@ package org.apache.cloudstack.api.command; import com.cloud.utils.HttpUtils; import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.saml.SAML2AuthManager; -import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.apache.cloudstack.saml.SAMLUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,9 +48,6 @@ public class SAML2LogoutAPIAuthenticatorCmdTest { SAML2AuthManager samlAuthManager; @Mock - ConfigurationDao configDao; - - @Mock HttpSession session; @Mock @@ -72,19 +68,10 @@ public class SAML2LogoutAPIAuthenticatorCmdTest { managerField.setAccessible(true); managerField.set(cmd, samlAuthManager); - Field configDaoField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_configDao"); - configDaoField.setAccessible(true); - configDaoField.set(cmd, configDao); - String spId = "someSPID"; String url = "someUrl"; X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); - Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); - Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); - Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); - Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url); Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); - Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString"); cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString()); http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/server/src/com/cloud/api/ApiServer.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index cf719c0..4da8b1e 100644 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -1062,8 +1062,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer final SecureRandom sesssionKeyRandom = new SecureRandom(); final byte sessionKeyBytes[] = new byte[20]; sesssionKeyRandom.nextBytes(sessionKeyBytes); - final String sessionKey = Base64.encodeBase64String(sessionKeyBytes); - session.setAttribute("sessionkey", sessionKey); + final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes); + session.setAttribute(ApiConstants.SESSIONKEY, sessionKey); return createLoginResponse(session); } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/server/src/com/cloud/api/ApiServlet.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 3b3a0be..2a2844e 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -238,7 +238,7 @@ public class ApiServlet extends HttpServlet { userId = (Long)session.getAttribute("userid"); final String account = (String)session.getAttribute("account"); final Object accountObj = session.getAttribute("accountobj"); - final String sessionKey = (String)session.getAttribute("sessionkey"); + final String sessionKey = (String)session.getAttribute(ApiConstants.SESSIONKEY); final String[] sessionKeyParam = (String[])params.get(ApiConstants.SESSIONKEY); if ((sessionKeyParam == null) || (sessionKey == null) || !sessionKey.equals(sessionKeyParam[0])) { try { http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/server/src/com/cloud/configuration/Config.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 2352313..ca89881 100644 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -1385,78 +1385,6 @@ public enum Config { "300000", "The allowable clock difference in milliseconds between when an SSO login request is made and when it is received.", null), - SAMLIsPluginEnabled( - "Advanced", - ManagementServer.class, - Boolean.class, - "saml2.enabled", - "false", - "Set it to true to enable SAML SSO plugin", - null), - SAMLUserDomain( - "Advanced", - ManagementServer.class, - String.class, - "saml2.default.domainid", - "1", - "The default domain UUID to use when creating users from SAML SSO", - null), - SAMLCloudStackRedirectionUrl( - "Advanced", - ManagementServer.class, - String.class, - "saml2.redirect.url", - "http://localhost:8080/client", - "The CloudStack UI url the SSO should redirected to when successful", - null), - SAMLServiceProviderID( - "Advanced", - ManagementServer.class, - String.class, - "saml2.sp.id", - "org.apache.cloudstack", - "SAML2 Service Provider Identifier String", - null), - SAMLServiceProviderSingleSignOnURL( - "Advanced", - ManagementServer.class, - String.class, - "saml2.sp.sso.url", - "http://localhost:8080/client/api?command=samlSso", - "SAML2 CloudStack Service Provider Single Sign On URL", - null), - SAMLServiceProviderSingleLogOutURL( - "Advanced", - ManagementServer.class, - String.class, - "saml2.sp.slo.url", - "http://localhost:8080/client/api?command=samlSlo", - "SAML2 CloudStack Service Provider Single Log Out URL", - null), - SAMLIdentityProviderID( - "Advanced", - ManagementServer.class, - String.class, - "saml2.idp.id", - "https://openidp.feide.no", - "SAML2 Identity Provider Identifier String", - null), - SAMLIdentityProviderMetadataURL( - "Advanced", - ManagementServer.class, - String.class, - "saml2.idp.metadata.url", - "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php", - "SAML2 Identity Provider Metadata XML Url", - null), - SAMLTimeout( - "Advanced", - ManagementServer.class, - Long.class, - "saml2.timeout", - "30000", - "SAML2 IDP Metadata Downloading and parsing etc. activity timeout in milliseconds", - null), //NetworkType("Hidden", ManagementServer.class, String.class, "network.type", "vlan", "The type of network that this deployment will use.", "vlan,direct"), RouterRamSize("Hidden", NetworkOrchestrationService.class, Integer.class, "router.ram.size", "256", "Default RAM for router VM (in MB).", null), http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/setup/db/db/schema-451to452-cleanup.sql ---------------------------------------------------------------------- diff --git a/setup/db/db/schema-451to452-cleanup.sql b/setup/db/db/schema-451to452-cleanup.sql new file mode 100644 index 0000000..9f5e62a --- /dev/null +++ b/setup/db/db/schema-451to452-cleanup.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema cleanup from 4.5.1 to 4.5.2; +--; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/setup/db/db/schema-451to452.sql ---------------------------------------------------------------------- diff --git a/setup/db/db/schema-451to452.sql b/setup/db/db/schema-451to452.sql new file mode 100644 index 0000000..5c89008 --- /dev/null +++ b/setup/db/db/schema-451to452.sql @@ -0,0 +1,35 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade from 4.5.1 to 4.5.2; +--; + +DELETE FROM `cloud`.`configuration` WHERE name like 'saml%'; + +ALTER TABLE `cloud`.`user` ADD COLUMN `external_entity` text DEFAULT NULL COMMENT "reference to external federation entity"; + +DROP TABLE IF EXISTS `cloud`.`saml_token`; +CREATE TABLE `cloud`.`saml_token` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(255) UNIQUE NOT NULL COMMENT 'The Authn Unique Id', + `domain_id` bigint unsigned DEFAULT NULL, + `entity` text NOT NULL COMMENT 'Identity Provider Entity Id', + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_saml_token__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/tools/apidoc/gen_toc.py ---------------------------------------------------------------------- diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index e53d69d..cb26e2b 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -115,6 +115,9 @@ known_categories = { 'logout': 'Authentication', 'saml': 'Authentication', 'getSPMetadata': 'Authentication', + 'listIdps': 'Authentication', + 'authorizeSamlSso': 'Authentication', + 'listSamlAuthorization': 'Authentication', 'Capacity': 'System Capacity', 'NetworkDevice': 'Network Device', 'ExternalLoadBalancer': 'Ext Load Balancer', http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/css/cloudstack3.css ---------------------------------------------------------------------- diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index a4e2a2a..2bcd5e5 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -369,7 +369,7 @@ body.login { .login .select-language select { width: 260px; border: 1px solid #808080; - margin-top: 30px; + margin-top: 20px; /*+border-radius:4px;*/ -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -460,14 +460,12 @@ body.login { background: transparent url(../images/sprites.png) -563px -747px; cursor: pointer; border: none; - margin: 7px 120px 0 -1px; text-align: center; width: 60px; height: 15px; display: block; color: #FFFFFF; font-weight: bold; - float: left; text-indent: -1px; /*+text-shadow:0px 1px 2px #000000;*/ -moz-text-shadow: 0px 1px 2px #000000; @@ -12749,6 +12747,14 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -196px -704px; } +.configureSamlAuthorization .icon { + background-position: -165px -122px; +} + +.configureSamlAuthorization:hover .icon { + background-position: -165px -704px; +} + .viewConsole .icon { background-position: -231px -2px; } @@ -12972,13 +12978,6 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it border-radius: 4px; border-radius: 4px 4px 4px 4px; border: 1px solid #AFAFAF; - -moz-box-shadow: inset 0px 1px #727272; - -webkit-box-shadow: inset 0px 1px #727272; - -o-box-shadow: inset 0px 1px #727272; - box-shadow: inset 0px 1px #727272; - -moz-box-shadow: inset 0px 1px 0px #727272; - -webkit-box-shadow: inset 0px 1px 0px #727272; - -o-box-shadow: inset 0px 1px 0px #727272; } .manual-account-details > *:nth-child(even) { http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/dictionary.jsp ---------------------------------------------------------------------- diff --git a/ui/dictionary.jsp b/ui/dictionary.jsp index 63d22bd..7d17267 100644 --- a/ui/dictionary.jsp +++ b/ui/dictionary.jsp @@ -143,6 +143,7 @@ dictionary = { 'label.action.cancel.maintenance.mode': '<fmt:message key="label.action.cancel.maintenance.mode" />', 'label.action.cancel.maintenance.mode.processing': '<fmt:message key="label.action.cancel.maintenance.mode.processing" />', 'label.action.change.password': '<fmt:message key="label.action.change.password" />', +'label.action.configure.samlauthorization': '<fmt:message key="label.action.configure.samlauthorization" />', 'label.action.change.service': '<fmt:message key="label.action.change.service" />', 'label.action.change.service.processing': '<fmt:message key="label.action.change.service.processing" />', 'label.action.copy.ISO': '<fmt:message key="label.action.copy.ISO" />', @@ -764,7 +765,9 @@ dictionary = { 'label.local.storage': '<fmt:message key="label.local.storage" />', 'label.login': '<fmt:message key="label.login" />', 'label.logout': '<fmt:message key="label.logout" />', -'label.saml.login': '<fmt:message key="label.saml.login" />', +'label.saml.enable': '<fmt:message key="label.saml.enable" />', +'label.saml.entity': '<fmt:message key="label.saml.entity" />', +'label.add.LDAP.account': '<fmt:message key="label.add.LDAP.account" />', 'label.lun': '<fmt:message key="label.lun" />', 'label.LUN.number': '<fmt:message key="label.LUN.number" />', 'label.make.project.owner': '<fmt:message key="label.make.project.owner" />', http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/index.jsp ---------------------------------------------------------------------- diff --git a/ui/index.jsp b/ui/index.jsp index 19c2dbf..e062799 100644 --- a/ui/index.jsp +++ b/ui/index.jsp @@ -51,28 +51,45 @@ <form> <div class="logo"></div> <div class="fields"> - <!-- User name --> - <div class="field username"> - <label for="username"><fmt:message key="label.username"/></label> - <input type="text" name="username" class="required" /> + <div id="login-dropdown"> + <select id="login-options" style="width: 260px"> + <option value="cloudstack-login">Local <fmt:message key="label.login"/></option> + </select> </div> - <!-- Password --> - <div class="field password"> - <label for="password"><fmt:message key="label.password"/></label> - <input type="password" name="password" class="required" autocomplete="off" /> + + <div id="cloudstack-login"> + <!-- User name --> + <div class="field username"> + <label for="username"><fmt:message key="label.username"/></label> + <input type="text" name="username" class="required" /> + </div> + <!-- Password --> + <div class="field password"> + <label for="password"><fmt:message key="label.password"/></label> + <input type="password" name="password" class="required" autocomplete="off" /> + </div> + <!-- Domain --> + <div class="field domain"> + <label for="domain"><fmt:message key="label.domain"/></label> + <input type="text" name="domain" /> + </div> </div> - <!-- Domain --> - <div class="field domain"> - <label for="domain"><fmt:message key="label.domain"/></label> - <input type="text" name="domain" /> + + <div id="saml-login"> + <div class="field domain"> + <label for="saml-domain"><fmt:message key="label.domain"/></label> + <input id="saml-domain" type="text" name="saml-domain" /> + </div> + </div> + + <div id="login-submit"> + <!-- Submit (login) --> + <input id="login-submit" type="submit" value="<fmt:message key="label.login"/>" /> </div> - <!-- Submit (login) --> - <input type="submit" value="<fmt:message key="label.login"/>" /> - <div id="saml-login"><input type="samlsubmit" value="<fmt:message key="label.saml.login"/>"/></div> <!-- Select language --> <div class="select-language"> <select name="language"> - <option value=""></option> <!-- when this blank option is selected, browser's default language will be used --> + <option value=""></option> <!-- when this blank option is selected, default language of the browser will be used --> <option value="en"><fmt:message key="label.lang.english"/></option> <option value="ja_JP"><fmt:message key="label.lang.japanese"/></option> <option value="zh_CN"><fmt:message key="label.lang.chinese"/></option> http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/accounts.js ---------------------------------------------------------------------- diff --git a/ui/scripts/accounts.js b/ui/scripts/accounts.js index 9e47d11..b63d184 100644 --- a/ui/scripts/accounts.js +++ b/ui/scripts/accounts.js @@ -1223,6 +1223,102 @@ } }, + configureSamlAuthorization: { + label: 'label.action.configure.samlauthorization', + messages: { + notification: function(args) { + return 'label.action.configure.samlauthorization'; + } + }, + action: { + custom: function(args) { + var start = args.start; + var complete = args.complete; + var context = args.context; + + if (g_idpList) { + $.ajax({ + url: createURL('listSamlAuthorization'), + data: { + userid: context.users[0].id, + }, + success: function(json) { + var authorization = json.listsamlauthorizationsresponse.samlauthorization[0]; + cloudStack.dialog.createForm({ + form: { + title: 'label.action.configure.samlauthorization', + fields: { + samlEnable: { + label: 'label.saml.enable', + docID: 'helpSamlEnable', + isBoolean: true, + isChecked: authorization.status, + validation: { + required: false + } + }, + samlEntity: { + label: 'label.saml.entity', + docID: 'helpSamlEntity', + validation: { + required: false + }, + select: function(args) { + var items = []; + $(g_idpList).each(function() { + items.push({ + id: this.id, + description: this.orgName + }); + }); + args.response.success({ + data: items + }); + args.$select.change(function() { + $('select[name="samlEntity"] option[value="' + authorization.idpid + '"]').attr("selected", "selected"); + }); + } + } + } + }, + after: function(args) { + start(); + var enableSaml = false; + var idpId = ''; + if (args.data.hasOwnProperty('samlEnable')) { + enableSaml = (args.data.samlEnable === 'on'); + } + if (args.data.hasOwnProperty('samlEntity')) { + idpId = args.data.samlEntity; + } + $.ajax({ + url: createURL('authorizeSamlSso'), + data: { + userid: context.users[0].id, + enable: enableSaml, + entityid: idpId + }, + type: "POST", + success: function(json) { + complete(); + }, + error: function(json) { + complete({ error: parseXMLHttpResponse(json) }); + } + }); + } + }); + }, + error: function(json) { + complete({ error: parseXMLHttpResponse(json) }); + } + }); + + } + } + } + }, + generateKeys: { label: 'label.action.generate.keys', messages: { @@ -1797,6 +1893,9 @@ allowedActions.push("edit"); allowedActions.push("changePassword"); allowedActions.push("generateKeys"); + if (g_idpList) { + allowedActions.push("configureSamlAuthorization"); + } if (!(jsonObj.domain == "ROOT" && jsonObj.account == "admin" && jsonObj.accounttype == 1)) { //if not system-generated default admin account user if (jsonObj.state == "enabled") allowedActions.push("disable"); @@ -1818,6 +1917,9 @@ allowedActions.push("changePassword"); allowedActions.push("generateKeys"); + if (g_idpList) { + allowedActions.push("configureSamlAuthorization"); + } } } return allowedActions; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/accountsWizard.js ---------------------------------------------------------------------- diff --git a/ui/scripts/accountsWizard.js b/ui/scripts/accountsWizard.js index 82e7eab..7ea5eaa 100644 --- a/ui/scripts/accountsWizard.js +++ b/ui/scripts/accountsWizard.js @@ -162,8 +162,34 @@ validation: { required: false } + }, + samlEnable: { + label: 'label.saml.enable', + docID: 'helpSamlEnable', + isBoolean: true, + validation: { + required: false + } + }, + samlEntity: { + label: 'label.saml.entity', + docID: 'helpSamlEntity', + validation: { + required: false + }, + select: function(args) { + var items = []; + $(g_idpList).each(function() { + items.push({ + id: this.id, + description: this.orgName + }); + }); + args.response.success({ + data: items + }); + } } - }, action: function(args) { @@ -218,6 +244,18 @@ array1.push("&group=" + args.groupname); } + var authorizeUsersForSamlSSO = function (users, entity) { + for (var i = 0; i < users.length; i++) { + $.ajax({ + url: createURL('authorizeSamlSso&enable=true&userid=' + users[i].id + "&entityid=" + entity), + error: function(XMLHttpResponse) { + args.response.error(parseXMLHttpResponse(XMLHttpResponse)); + } + }); + } + return; + }; + if (ldapStatus) { if (args.groupname) { $.ajax({ @@ -225,6 +263,13 @@ dataType: "json", type: "POST", async: false, + success: function (json) { + if (json.ldapuserresponse && args.data.samlEnable && args.data.samlEnable === 'on') { + cloudStack.dialog.notice({ + message: "Unable to find users IDs to enable SAML Single Sign On, kindly enable it manually." + }); + } + }, error: function(XMLHttpResponse) { args.response.error(parseXMLHttpResponse(XMLHttpResponse)); } @@ -235,6 +280,14 @@ dataType: "json", type: "POST", async: false, + success: function(json) { + if (args.data.samlEnable && args.data.samlEnable === 'on') { + var users = json.createaccountresponse.account.user; + var entity = args.data.samlEntity; + if (users && entity) + authorizeUsersForSamlSSO(users, entity); + } + }, error: function(XMLHttpResponse) { args.response.error(parseXMLHttpResponse(XMLHttpResponse)); } @@ -246,6 +299,14 @@ dataType: "json", type: "POST", async: false, + success: function(json) { + if (args.data.samlEnable && args.data.samlEnable === 'on') { + var users = json.createaccountresponse.account.user; + var entity = args.data.samlEntity; + if (users && entity) + authorizeUsersForSamlSSO(users, entity); + } + }, error: function(XMLHttpResponse) { args.response.error(parseXMLHttpResponse(XMLHttpResponse)); } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/cloudStack.js ---------------------------------------------------------------------- diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index 55200b6..0e0a71a 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -115,7 +115,7 @@ cookieValue = cookieValue.slice(1, cookieValue.length-1); $.cookie(cookieName, cookieValue, { expires: 1 }); } - return cookieValue; + return decodeURIComponent(cookieValue); }; unBoxCookieValue('sessionkey'); // if sessionkey cookie exists use this to set g_sessionKey @@ -353,6 +353,17 @@ }, samlLoginAction: function(args) { + g_sessionKey = null; + g_username = null; + g_account = null; + g_domainid = null; + g_timezoneoffset = null; + g_timezone = null; + g_supportELB = null; + g_kvmsnapshotenabled = null; + g_regionsecondaryenabled = null; + g_loginCmdText = null; + $.cookie('JSESSIONID', null); $.cookie('sessionkey', null); $.cookie('username', null); @@ -360,7 +371,14 @@ $.cookie('domainid', null); $.cookie('role', null); $.cookie('timezone', null); - window.location.href = createURL('samlSso'); + var url = 'samlSso'; + if (args.data.idpid) { + url = url + '&idpid=' + args.data.idpid; + } + if (args.data.domain) { + url = url + '&domain=' + args.data.domain; + } + window.location.href = createURL(url); }, // Show cloudStack main UI widget http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/docs.js ---------------------------------------------------------------------- diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js index 53c31c2..809c398 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -1261,6 +1261,14 @@ cloudStack.docs = { desc: 'The group name from which you want to import LDAP users', externalLink: '' }, + helpSamlEnable: { + desc: 'Enable SAML Single Sign On for the user(s)', + externalLink: '' + }, + helpSamlEntity: { + desc: 'Choose the SAML Identity Provider Entity ID with which you want to enable the Single Sign On for the user(s)', + externalLink: '' + }, helpVpcOfferingName: { desc: 'Any desired name for the VPC offering', externalLink: '' http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/sharedFunctions.js ---------------------------------------------------------------------- diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index e8f7cc9..c29956a 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -32,6 +32,7 @@ var g_regionsecondaryenabled = null; var g_userPublicTemplateEnabled = "true"; var g_cloudstackversion = null; var g_queryAsyncJobResultInterval = 3000; +var g_idpList = null; //keyboard keycode var keycode_Enter = 13; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/ui-custom/accountsWizard.js ---------------------------------------------------------------------- diff --git a/ui/scripts/ui-custom/accountsWizard.js b/ui/scripts/ui-custom/accountsWizard.js index 3259227..cfbe930 100644 --- a/ui/scripts/ui-custom/accountsWizard.js +++ b/ui/scripts/ui-custom/accountsWizard.js @@ -271,6 +271,11 @@ delete args.informationNotInLdap.ldapGroupName; } + if (g_idpList == null) { + delete args.informationNotInLdap.samlEnable; + delete args.informationNotInLdap.samlEntity; + } + var informationNotInLdap = cloudStack.dialog.createForm({ context: context, noDialog: true, http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/ui/scripts/ui-custom/login.js ---------------------------------------------------------------------- diff --git a/ui/scripts/ui-custom/login.js b/ui/scripts/ui-custom/login.js index 194c881..7c7e923 100644 --- a/ui/scripts/ui-custom/login.js +++ b/ui/scripts/ui-custom/login.js @@ -94,54 +94,120 @@ $inputs.filter(':first').addClass('first-input').focus(); // Login action - $login.find('input[type=submit]').click(function() { - if (!$form.valid()) return false; - - var data = cloudStack.serializeForm($form); - - args.loginAction({ - data: data, - response: { - success: function(args) { - $login.remove(); - $('html body').removeClass('login'); - complete({ - user: args.data.user - }); - }, - error: function(args) { - cloudStack.dialog.notice({ - message: args - }); + var selectedLogin = 'cloudstack'; + $login.find('#login-submit').click(function() { + if (selectedLogin === 'cloudstack') { + // CloudStack Local Login + if (!$form.valid()) return false; + + var data = cloudStack.serializeForm($form); + + args.loginAction({ + data: data, + response: { + success: function(args) { + $login.remove(); + $('html body').removeClass('login'); + complete({ + user: args.data.user + }); + }, + error: function(args) { + cloudStack.dialog.notice({ + message: args + }); + } } - } - }); - + }); + } else if (selectedLogin === 'saml') { + // SAML + args.samlLoginAction({ + data: {'idpid': $login.find('#login-options').find(':selected').val(), + 'domain': $login.find('#saml-domain').val()} + }); + } return false; }); - // SAML Login action - $login.find('input[type=samlsubmit]').click(function() { - args.samlLoginAction({ - }); + // Show SAML button if only SP is configured + $login.find('#login-dropdown').hide(); + $login.find('#saml-login').hide(); + $login.find('#cloudstack-login').hide(); + + var toggleLoginView = function (selectedOption) { + $login.find('#login-submit').show(); + if (selectedOption === '') { + $login.find('#saml-login').hide(); + $login.find('#cloudstack-login').hide(); + $login.find('#login-submit').hide(); + selectedLogin = 'none'; + } else if (selectedOption === 'cloudstack-login') { + $login.find('#saml-login').hide(); + $login.find('#cloudstack-login').show(); + selectedLogin = 'cloudstack'; + } else { + $login.find('#saml-login').show(); + $login.find('#cloudstack-login').hide(); + selectedLogin = 'saml'; + } + }; + + $login.find('#login-options').change(function() { + var selectedOption = $login.find('#login-options').find(':selected').val(); + toggleLoginView(selectedOption); + if (selectedOption && selectedOption !== '') { + $.cookie('login-option', selectedOption); + } }); - // Show SAML button if only SP is configured - $login.find("#saml-login").hide(); $.ajax({ - type: "GET", - url: createURL("getSPMetadata"), - dataType: "json", + type: 'GET', + url: createURL('listIdps'), + dataType: 'json', async: false, success: function(data, textStatus, xhr) { if (xhr.status === 200) { - $login.find('#saml-login').show(); + $login.find('#login-dropdown').show(); + $login.find('#login-submit').hide(); } else { - $login.find('#saml-login').hide(); + $login.find('#cloudstack-login').show(); + $login.find('#login-submit').show(); + return; + } + + $login.find('#login-options') + .append($('<option>', { + value: '', + text: '--- Select Identity Provider -- ', + selected: true + })); + + if (data.listidpsresponse && data.listidpsresponse.idp) { + var idpList = data.listidpsresponse.idp.sort(function (a, b) { + return a.orgName.localeCompare(b.orgName); + }); + g_idpList = idpList; + $.each(idpList, function(index, idp) { + $login.find('#login-options') + .append($('<option>', { + value: idp.id, + text: idp.orgName + })); + }); + } + + var loginOption = $.cookie('login-option'); + if (loginOption) { + var option = $login.find('#login-options option[value="' + loginOption + '"]'); + if (option.length > 0) { + option.prop('selected', true); + toggleLoginView(loginOption); + } } }, error: function(xhr) { $login.find('#saml-login').hide(); + $login.find('#cloudstack-login').show(); } }); http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java ---------------------------------------------------------------------- diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java deleted file mode 100644 index a6d2d34..0000000 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ /dev/null @@ -1,330 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -// - -package org.apache.cloudstack.utils.auth; - -import com.cloud.utils.HttpUtils; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.log4j.Logger; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.x509.X509V1CertificateGenerator; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.opensaml.Configuration; -import org.opensaml.common.SAMLVersion; -import org.opensaml.common.xml.SAMLConstants; -import org.opensaml.saml2.core.AuthnContextClassRef; -import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; -import org.opensaml.saml2.core.AuthnRequest; -import org.opensaml.saml2.core.Issuer; -import org.opensaml.saml2.core.LogoutRequest; -import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.NameIDPolicy; -import org.opensaml.saml2.core.NameIDType; -import org.opensaml.saml2.core.RequestedAuthnContext; -import org.opensaml.saml2.core.Response; -import org.opensaml.saml2.core.SessionIndex; -import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; -import org.opensaml.saml2.core.impl.AuthnRequestBuilder; -import org.opensaml.saml2.core.impl.IssuerBuilder; -import org.opensaml.saml2.core.impl.LogoutRequestBuilder; -import org.opensaml.saml2.core.impl.NameIDBuilder; -import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; -import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; -import org.opensaml.saml2.core.impl.SessionIndexBuilder; -import org.opensaml.xml.ConfigurationException; -import org.opensaml.xml.XMLObject; -import org.opensaml.xml.io.Marshaller; -import org.opensaml.xml.io.MarshallingException; -import org.opensaml.xml.io.Unmarshaller; -import org.opensaml.xml.io.UnmarshallerFactory; -import org.opensaml.xml.io.UnmarshallingException; -import org.opensaml.xml.signature.SignatureConstants; -import org.opensaml.xml.util.Base64; -import org.opensaml.xml.util.XMLHelper; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.xml.sax.SAXException; - -import javax.security.auth.x500.X500Principal; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Security; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; - -public class SAMLUtils { - public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); - - public static final String SAML_RESPONSE = "SAMLResponse"; - public static final String SAML_NS = "SAML-"; - public static final String SAML_NAMEID = "SAML_NAMEID"; - public static final String SAML_SESSION = "SAML_SESSION"; - public static final String SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR"; - public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT"; - - public static String createSAMLId(String uid) { - if (uid == null) { - return null; - } - String hash = DigestUtils.sha256Hex(uid); - String samlUuid = SAML_NS + hash; - return samlUuid.substring(0, 40); - } - - public static boolean checkSAMLUser(String uuid, String username) { - if (uuid == null || uuid.isEmpty() || username == null || username.isEmpty()) { - return false; - } - return uuid.startsWith(SAML_NS) && createSAMLId(username).equals(uuid); - } - - public static String generateSecureRandomId() { - return new BigInteger(160, new SecureRandom()).toString(32); - } - - public static AuthnRequest buildAuthnRequestObject(String spId, String idpUrl, String consumerUrl) { - String authnId = generateSecureRandomId(); - // Issuer object - IssuerBuilder issuerBuilder = new IssuerBuilder(); - Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(spId); - - // NameIDPolicy - NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); - NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); - nameIdPolicy.setFormat(NameIDType.PERSISTENT); - nameIdPolicy.setSPNameQualifier(spId); - nameIdPolicy.setAllowCreate(true); - - // AuthnContextClass - AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); - AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( - SAMLConstants.SAML20_NS, - "AuthnContextClassRef", "saml"); - authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); - - // AuthnContex - RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); - RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); - requestedAuthnContext - .setComparison(AuthnContextComparisonTypeEnumeration.EXACT); - requestedAuthnContext.getAuthnContextClassRefs().add( - authnContextClassRef); - - // Creation of AuthRequestObject - AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); - AuthnRequest authnRequest = authRequestBuilder.buildObject(); - authnRequest.setID(authnId); - authnRequest.setDestination(idpUrl); - authnRequest.setVersion(SAMLVersion.VERSION_20); - authnRequest.setForceAuthn(false); - authnRequest.setIsPassive(false); - authnRequest.setIssuer(issuer); - authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); - authnRequest.setAssertionConsumerServiceURL(consumerUrl); - authnRequest.setProviderName(spId); - authnRequest.setNameIDPolicy(nameIdPolicy); - authnRequest.setRequestedAuthnContext(requestedAuthnContext); - - return authnRequest; - } - - public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) { - IssuerBuilder issuerBuilder = new IssuerBuilder(); - Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(spId); - - SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject(); - sessionIndexElement.setSessionIndex(sessionIndex); - - NameID nameID = new NameIDBuilder().buildObject(); - nameID.setValue(sessionNameId.getValue()); - nameID.setFormat(sessionNameId.getFormat()); - - LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject(); - logoutRequest.setID(generateSecureRandomId()); - logoutRequest.setDestination(logoutUrl); - logoutRequest.setVersion(SAMLVersion.VERSION_20); - logoutRequest.setIssueInstant(new DateTime()); - logoutRequest.setIssuer(issuer); - logoutRequest.getSessionIndexes().add(sessionIndexElement); - logoutRequest.setNameID(nameID); - return logoutRequest; - } - - public static String encodeSAMLRequest(XMLObject authnRequest) - throws MarshallingException, IOException { - Marshaller marshaller = Configuration.getMarshallerFactory() - .getMarshaller(authnRequest); - Element authDOM = marshaller.marshall(authnRequest); - StringWriter requestWriter = new StringWriter(); - XMLHelper.writeNode(authDOM, requestWriter); - String requestMessage = requestWriter.toString(); - Deflater deflater = new Deflater(Deflater.DEFLATED, true); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); - deflaterOutputStream.write(requestMessage.getBytes()); - deflaterOutputStream.close(); - String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); - encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, HttpUtils.UTF_8).trim(); - return encodedRequestMessage; - } - - public static Response decodeSAMLResponse(String responseMessage) - throws ConfigurationException, ParserConfigurationException, - SAXException, IOException, UnmarshallingException { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); - byte[] base64DecodedResponse = Base64.decode(responseMessage); - Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); - Element element = document.getDocumentElement(); - UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); - Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); - return (Response) unmarshaller.unmarshall(element); - } - - public static String generateSAMLRequestSignature(String urlEncodedString, PrivateKey signingKey) - throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException { - if (signingKey == null) { - return urlEncodedString; - } - String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1, HttpUtils.UTF_8); - Signature signature = Signature.getInstance("SHA1withRSA"); - signature.initSign(signingKey); - signature.update(url.getBytes()); - String signatureString = Base64.encodeBytes(signature.sign(), Base64.DONT_BREAK_LINES); - if (signatureString != null) { - return url + "&Signature=" + URLEncoder.encode(signatureString, HttpUtils.UTF_8); - } - return url; - } - - public static KeyFactory getKeyFactory() { - KeyFactory keyFactory = null; - try { - Security.addProvider(new BouncyCastleProvider()); - keyFactory = KeyFactory.getInstance("RSA", "BC"); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return keyFactory; - } - - public static String savePublicKey(PublicKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded())); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static String savePrivateKey(PrivateKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key, - PKCS8EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded())); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static PublicKey loadPublicKey(String publicKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePublic(x509KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static PrivateKey loadPrivateKey(String privateKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey); - PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePrivate(pkscs8KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException { - Security.addProvider(new BouncyCastleProvider()); - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); - keyPairGenerator.initialize(4096, new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } - - public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { - DateTime now = DateTime.now(DateTimeZone.UTC); - X500Principal dnName = new X500Principal("CN=ApacheCloudStack"); - X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setSubjectDN(dnName); - certGen.setIssuerDN(dnName); - certGen.setNotBefore(now.minusDays(1).toDate()); - certGen.setNotAfter(now.plusYears(3).toDate()); - certGen.setPublicKey(keyPair.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen.generate(keyPair.getPrivate(), "BC"); - } - -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/107595a6/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java ---------------------------------------------------------------------- diff --git a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java b/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java deleted file mode 100644 index bebfd13..0000000 --- a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java +++ /dev/null @@ -1,91 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -// - -package org.apache.cloudstack.utils.auth; - -import junit.framework.TestCase; -import org.junit.Test; -import org.opensaml.saml2.core.AuthnRequest; -import org.opensaml.saml2.core.LogoutRequest; -import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.impl.NameIDBuilder; - -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; - -public class SAMLUtilsTest extends TestCase { - - @Test - public void testSAMLId() throws Exception { - assertEquals(SAMLUtils.createSAMLId(null), null); - assertEquals(SAMLUtils.createSAMLId("someUserName"), "SAML-305e19dd2581f33fd90b3949298ec8b17de"); - - assertTrue(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someUserName")); - assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someOtherUserName")); - assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId(null), "someOtherUserName")); - assertFalse(SAMLUtils.checkSAMLUser("randomUID", "randomUID")); - assertFalse(SAMLUtils.checkSAMLUser(null, null)); - } - - @Test - public void testGenerateSecureRandomId() throws Exception { - assertTrue(SAMLUtils.generateSecureRandomId().length() > 0); - } - - @Test - public void testBuildAuthnRequestObject() throws Exception { - String consumerUrl = "http://someurl.com"; - String idpUrl = "http://idp.domain.example"; - String spId = "cloudstack"; - AuthnRequest req = SAMLUtils.buildAuthnRequestObject(spId, idpUrl, consumerUrl); - assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl); - assertEquals(req.getDestination(), idpUrl); - assertEquals(req.getIssuer().getValue(), spId); - } - - @Test - public void testBuildLogoutRequest() throws Exception { - String logoutUrl = "http://logoutUrl"; - String spId = "cloudstack"; - String sessionIndex = "12345"; - String nameIdString = "someNameID"; - NameID sessionNameId = new NameIDBuilder().buildObject(); - sessionNameId.setValue(nameIdString); - LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, sessionNameId, sessionIndex); - assertEquals(req.getDestination(), logoutUrl); - assertEquals(req.getIssuer().getValue(), spId); - assertEquals(req.getNameID().getValue(), nameIdString); - assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex); - } - - @Test - public void testX509Helpers() throws Exception { - KeyPair keyPair = SAMLUtils.generateRandomKeyPair(); - - String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate()); - String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic()); - - PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString); - PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString); - - assertTrue(privateKey.equals(keyPair.getPrivate())); - assertTrue(publicKey.equals(keyPair.getPublic())); - } -} \ No newline at end of file