Merge branch 'saml2'

Implements CLOUDSTACK-7083

Branch: saml2
Proposal: http://markmail.org/message/4ba4ztmqpud3l4uo
JIRA ticket: https://issues.apache.org/jira/browse/CLOUDSTACK-7083
FS: https://cwiki.apache.org/confluence/display/CLOUDSTACK/SAML+2.0+Plugin
Unit tests: Tests for each auth cmd class, SAMLUtils and SAMLAuthenticator, fixes unit test for ApiServlet
Build status: clean build works with unit tests, testing using mvn3.0.5 and jdk 1.7
This commit is contained in:
Rohit Yadav 2014-08-28 19:57:25 +02:00
commit 97ed5ff636
38 changed files with 2049 additions and 208 deletions

View File

@ -35,6 +35,11 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${cs.servlet.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-db</artifactId>

View File

@ -514,6 +514,7 @@ public class ApiConstants {
public static final String VMPROFILE_ID = "vmprofileid";
public static final String VMGROUP_ID = "vmgroupid";
public static final String CS_URL = "csurl";
public static final String IDP_URL = "idpurl";
public static final String SCALEUP_POLICY_IDS = "scaleuppolicyids";
public static final String SCALEDOWN_POLICY_IDS = "scaledownpolicyids";
public static final String SCALEUP_POLICIES = "scaleuppolicies";

View File

@ -14,23 +14,19 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.apache.cloudstack.api.ServerApiException;
package org.apache.cloudstack.api;
import com.cloud.exception.CloudAuthenticationException;
import javax.servlet.http.HttpSession;
import java.util.Map;
public interface ApiServerService {
public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId) throws ServerApiException;
public Long fetchDomainId(String domainUUID);
public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress,
Map<String, Object[]> requestParameters) throws CloudAuthenticationException;
public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress,
Map<String, Object[]> requestParameters) throws CloudAuthenticationException;
public void logoutUser(long userId);

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
package org.apache.cloudstack.api.auth;
import com.cloud.utils.component.PluggableService;

View File

@ -14,7 +14,7 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
package org.apache.cloudstack.api.auth;
public enum APIAuthenticationType {
LOGIN_API, LOGOUT_API

View File

@ -14,12 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
package org.apache.cloudstack.api.auth;
import org.apache.cloudstack.api.ServerApiException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Map;
/*
@ -36,6 +37,8 @@ public interface APIAuthenticator {
public String authenticate(String command, Map<String, Object[]> params,
HttpSession session, String remoteAddress, String responseType,
StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException;
public APIAuthenticationType getAPIType();
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators);
}

View File

@ -0,0 +1,25 @@
// 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.auth;
import com.cloud.utils.component.Adapter;
import java.util.List;
public interface PluggableAPIAuthenticator extends Adapter {
public List<Class<?>> getAuthCommands();
}

View File

@ -21,8 +21,11 @@
### CloudStack authentication commands
login=15
logout=15
### SAML SSO/SLO commands
samlsso=15
samlslo=15
getSPMetadata=15
### Account commands
createAccount=7

View File

@ -35,6 +35,12 @@
value="com.cloud.server.auth.UserAuthenticator" />
</bean>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="pluggableAPIAuthenticatorsRegistry" />
<property name="typeClass"
value=" org.apache.cloudstack.api.auth.PluggableAPIAuthenticator" />
</bean>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="securityCheckersRegistry" />
<property name="typeClass"

View File

@ -33,7 +33,14 @@
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="user.authenticators.order" />
<property name="excludeKey" value="user.authenticators.exclude" />
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,PLAINTEXT" />
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
</bean>
<bean id="pluggableAPIAuthenticatorsRegistry"
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="pluggableApi.authenticators.order" />
<property name="excludeKey" value="pluggableApi.authenticators.exclude" />
<property name="orderConfigDefault" value="SAML2Auth" />
</bean>
<bean id="userPasswordEncodersRegistry"

View File

@ -35,7 +35,17 @@
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>2.6.1</version>
<version>${cs.opensaml.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -25,8 +25,12 @@
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean id="SAML2UserAuthenticator" class="org.apache.cloudstack.SAML2UserAuthenticator">
<bean id="SAML2UserAuthenticator" class="org.apache.cloudstack.saml.SAML2UserAuthenticator">
<property name="name" value="SAML2"/>
</bean>
<bean id="SAML2Manager" class="org.apache.cloudstack.saml.SAML2AuthManagerImpl">
<property name="name" value="SAML2Auth"/>
</bean>
</beans>

View File

@ -0,0 +1,202 @@
// 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.api.response.ApiResponseSerializer;
import com.cloud.user.Account;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiErrorCode;
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.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.SAMLMetaDataResponse;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.log4j.Logger;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.NameIDFormat;
import org.opensaml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder;
import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder;
import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder;
import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder;
import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
import org.opensaml.xml.security.x509.BasicX509Credential;
import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory;
import org.w3c.dom.Document;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
@APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {})
public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthenticator {
public static final Logger s_logger = Logger.getLogger(GetServiceProviderMetaDataCmd.class.getName());
private static final String s_name = "spmetadataresponse";
@Inject
ApiServerService _apiServer;
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_TYPE_NORMAL;
}
@Override
public void execute() throws ServerApiException {
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly");
}
@Override
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletResponse resp) throws ServerApiException {
SAMLMetaDataResponse response = new SAMLMetaDataResponse();
response.setResponseName(getCommandName());
try {
DefaultBootstrap.bootstrap();
} catch (ConfigurationException | FactoryConfigurationError e) {
s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage());
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"OpenSAML Bootstrapping error while creating SP MetaData",
params, responseType));
}
EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
spEntityDescriptor.setEntityID(_samlAuthManager.getServiceProviderId());
SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
spSSODescriptor.setWantAssertionsSigned(true);
spSSODescriptor.setAuthnRequestsSigned(false);
X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
keyInfoGeneratorFactory.setEmitEntityCertificate(true);
KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject();
encKeyDescriptor.setUse(UsageType.ENCRYPTION);
KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject();
signKeyDescriptor.setUse(UsageType.SIGNING);
BasicX509Credential credential = new BasicX509Credential();
credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey());
try {
encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
//TODO: generate own pub/priv keys
//spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor);
//spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor);
} catch (SecurityException ignored) {
}
NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
nameIDFormat.setFormat(NameIDType.PERSISTENT);
spSSODescriptor.getNameIDFormats().add(nameIDFormat);
AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
assertionConsumerService.setIndex(0);
assertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
assertionConsumerService.setLocation(_samlAuthManager.getSpSingleSignOnUrl());
SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject();
ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
ssoService.setLocation(_samlAuthManager.getSpSingleLogOutUrl());
spSSODescriptor.getSingleLogoutServices().add(ssoService);
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor);
out.marshall(spEntityDescriptor, document);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringWriter stringWriter = new StringWriter();
StreamResult streamResult = new StreamResult(stringWriter);
DOMSource source = new DOMSource(document);
transformer.transform(source, streamResult);
stringWriter.close();
response.setMetadata(stringWriter.toString());
} catch (ParserConfigurationException | IOException | MarshallingException | TransformerException e) {
response.setMetadata("Error creating Service Provider MetaData XML: " + e.getMessage());
}
return ApiResponseSerializer.toSerializedString(response, responseType);
}
@Override
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGIN_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
for (PluggableAPIAuthenticator authManager: authenticators) {
if (authManager instanceof SAML2AuthManager) {
_samlAuthManager = (SAML2AuthManager) authManager;
}
}
if (_samlAuthManager == null) {
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
}
}
}

View File

@ -0,0 +1,303 @@
// 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.api.response.ApiResponseSerializer;
import com.cloud.configuration.Config;
import com.cloud.domain.Domain;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.db.EntityManager;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.LoginCmdResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.utils.auth.SAMLUtils;
import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeStatement;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.security.x509.BasicX509Credential;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureValidator;
import org.opensaml.xml.validation.ValidationException;
import org.xml.sax.SAXException;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
@APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName());
private static final String s_name = "loginresponse";
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true)
private String idpUrl;
@Inject
ApiServerService _apiServer;
@Inject
EntityManager _entityMgr;
@Inject
ConfigurationDao _configDao;
@Inject
DomainManager _domainMgr;
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getIdpUrl() {
return idpUrl;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_TYPE_NORMAL;
}
@Override
public void execute() throws ServerApiException {
// We should never reach here
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
}
private String buildAuthnRequestUrl(String idpUrl) {
String spId = _samlAuthManager.getServiceProviderId();
String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl();
String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl();
if (idpUrl != null) {
identityProviderUrl = idpUrl;
}
String redirectUrl = "";
try {
DefaultBootstrap.bootstrap();
AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(spId, identityProviderUrl, consumerUrl);
redirectUrl = identityProviderUrl + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest);
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) {
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
}
return redirectUrl;
}
public Response processSAMLResponse(String responseMessage) {
Response responseObject = null;
try {
DefaultBootstrap.bootstrap();
responseObject = SAMLUtils.decodeSAMLResponse(responseMessage);
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
s_logger.error("SAMLResponse processing error: " + e.getMessage());
}
return responseObject;
}
@Override
public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
try {
if (!params.containsKey("SAMLResponse")) {
String idpUrl = null;
final String[] idps = (String[])params.get(ApiConstants.IDP_URL);
if (idps != null && idps.length > 0) {
idpUrl = idps[0];
}
String redirectUrl = this.buildAuthnRequestUrl(idpUrl);
resp.sendRedirect(redirectUrl);
return "";
} else {
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
Response processedSAMLResponse = this.processSAMLResponse(samlResponse);
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Identity Provider send a non-successful authentication status code",
params, responseType));
}
if (_samlAuthManager.getIdpSigningKey() != null) {
Signature sig = processedSAMLResponse.getSignature();
BasicX509Credential credential = new BasicX509Credential();
credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey());
SignatureValidator validator = new SignatureValidator(credential);
try {
validator.validate(sig);
} catch (ValidationException e) {
s_logger.error("SAML Response's signature failed to be validated by IDP signing key:" + e.getMessage());
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"SAML Response's signature failed to be validated by IDP signing key",
params, responseType));
}
}
String uniqueUserId = null;
String accountName = _configDao.getValue(Config.SAMLUserAccountName.key());
String domainString = _configDao.getValue(Config.SAMLUserDomain.key());
Long domainId = -1L;
Domain domain = _domainMgr.getDomain(domainString);
if (domain != null) {
domainId = domain.getId();
} else {
try {
domainId = Long.parseLong(domainString);
} catch (NumberFormatException ignore) {
}
}
if (domainId == -1L) {
s_logger.error("The default domain ID for SAML users is not set correct, it should be a UUID");
}
String username = null;
String password = SAMLUtils.generateSecureRandomId(); // Random password
String firstName = "";
String lastName = "";
String timeZone = "";
String email = "";
Assertion assertion = processedSAMLResponse.getAssertions().get(0);
NameID nameId = assertion.getSubject().getNameID();
String sessionIndex = assertion.getAuthnStatements().get(0).getSessionIndex();
session.setAttribute(SAMLUtils.SAML_NAMEID, nameId);
session.setAttribute(SAMLUtils.SAML_SESSION, sessionIndex);
if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) {
username = nameId.getValue();
uniqueUserId = SAMLUtils.createSAMLId(username);
if (nameId.getFormat().equals(NameIDType.EMAIL)) {
email = username;
}
}
AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0);
List<Attribute> attributes = attributeStatement.getAttributes();
// Try capturing standard LDAP attributes
for (Attribute attribute: attributes) {
String attributeName = attribute.getName();
String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent();
if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) {
username = attributeValue;
uniqueUserId = SAMLUtils.createSAMLId(username);
} else if (attributeName.equalsIgnoreCase("givenName")) {
firstName = attributeValue;
} else if (attributeName.equalsIgnoreCase(("sn"))) {
lastName = attributeValue;
} else if (attributeName.equalsIgnoreCase("mail")) {
email = attributeValue;
}
}
User user = _entityMgr.findByUuid(User.class, uniqueUserId);
if (user == null && uniqueUserId != null && username != null
&& accountName != null && domainId != null) {
CallContext.current().setEventDetails("UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName);
user = _accountService.createUser(username, password, firstName, lastName, email, timeZone, accountName, domainId, uniqueUserId);
}
if (user != null) {
try {
if (_apiServer.verifyUser(user.getId())) {
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, user.getPassword(), domainId, null, remoteAddress, params);
resp.addCookie(new Cookie("userid", loginResponse.getUserId()));
resp.addCookie(new Cookie("domainid", loginResponse.getDomainId()));
resp.addCookie(new Cookie("role", loginResponse.getType()));
resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName()));
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
}
} catch (final CloudAuthenticationException ignored) {
}
}
}
} catch (IOException e) {
auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:");
auditTrailSb.append(e.getMessage());
}
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Unable to authenticate or retrieve user while performing SAML based SSO",
params, responseType));
}
@Override
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGIN_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
for (PluggableAPIAuthenticator authManager: authenticators) {
if (authManager instanceof SAML2AuthManager) {
_samlAuthManager = (SAML2AuthManager) authManager;
}
}
if (_samlAuthManager == null) {
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
}
}
}

View File

@ -0,0 +1,169 @@
// 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.api.response.ApiResponseSerializer;
import com.cloud.configuration.Config;
import com.cloud.user.Account;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiErrorCode;
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.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.LogoutCmdResponse;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.utils.auth.SAMLUtils;
import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.LogoutRequest;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.UnmarshallingException;
import org.xml.sax.SAXException;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@APICommand(name = "samlslo", description = "SAML Global Log Out API", responseObject = LogoutCmdResponse.class, entityType = {})
public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName());
private static final String s_name = "logoutresponse";
@Inject
ApiServerService _apiServer;
@Inject
ConfigurationDao _configDao;
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_TYPE_NORMAL;
}
@Override
public void execute() throws ServerApiException {
// We should never reach here
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
}
@Override
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
auditTrailSb.append("=== SAML SLO Logging out ===");
LogoutCmdResponse response = new LogoutCmdResponse();
response.setDescription("success");
response.setResponseName(getCommandName());
String responseString = ApiResponseSerializer.toSerializedString(response, responseType);
if (session == null) {
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
} catch (IOException ignored) {
}
return responseString;
}
try {
DefaultBootstrap.bootstrap();
} catch (ConfigurationException | FactoryConfigurationError e) {
s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage());
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"OpenSAML Bootstrapping error while creating SP MetaData",
params, responseType));
}
if (params != null && params.containsKey("SAMLResponse")) {
try {
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse);
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.INTERNAL_ERROR.getHttpCode(),
"SAML SLO LogoutResponse status is not Success",
params, responseType));
}
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
s_logger.error("SAMLResponse processing error: " + e.getMessage());
}
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
} catch (IOException ignored) {
}
return responseString;
}
NameID nameId = (NameID) session.getAttribute(SAMLUtils.SAML_NAMEID);
String sessionIndex = (String) session.getAttribute(SAMLUtils.SAML_SESSION);
if (nameId == null || sessionIndex == null) {
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
} catch (IOException ignored) {
}
return responseString;
}
LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex);
try {
String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
resp.sendRedirect(redirectUrl);
} catch (MarshallingException | IOException e) {
s_logger.error("SAML SLO error: " + e.getMessage());
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"SAML Single Logout Error",
params, responseType));
}
return responseString;
}
@Override
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGOUT_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
for (PluggableAPIAuthenticator authManager: authenticators) {
if (authManager instanceof SAML2AuthManager) {
_samlAuthManager = (SAML2AuthManager) authManager;
}
}
if (_samlAuthManager == null) {
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
}
}
}

View File

@ -0,0 +1,40 @@
// 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.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.BaseResponse;
public class SAMLMetaDataResponse extends BaseResponse {
@SerializedName("metadata")
@Param(description = "The Metadata XML")
private String metadata;
public SAMLMetaDataResponse() {
super();
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
}
}

View File

@ -0,0 +1,36 @@
// 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.saml;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import java.security.cert.X509Certificate;
public interface SAML2AuthManager extends PluggableAPIAuthenticator {
public String getServiceProviderId();
public String getIdentityProviderId();
public X509Certificate getIdpSigningKey();
public X509Certificate getIdpEncryptionKey();
public String getSpSingleSignOnUrl();
public String getIdpSingleSignOnUrl();
public String getSpSingleLogOutUrl();
public String getIdpSingleLogOutUrl();
}

View File

@ -0,0 +1,195 @@
// 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.saml;
import com.cloud.configuration.Config;
import com.cloud.utils.component.AdapterBase;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd;
import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
import org.springframework.stereotype.Component;
import javax.ejb.Local;
import javax.inject.Inject;
import javax.xml.stream.FactoryConfigurationError;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
@Component
@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class})
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager {
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
private String serviceProviderId;
private String identityProviderId;
private X509Certificate idpSigningKey;
private X509Certificate idpEncryptionKey;
private String spSingleSignOnUrl;
private String idpSingleSignOnUrl;
private String spSingleLogOutUrl;
private String idpSingleLogOutUrl;
private HTTPMetadataProvider idpMetaDataProvider;
@Inject
ConfigurationDao _configDao;
@Override
public boolean start() {
return isSAMLPluginEnabled() && setup();
}
private boolean setup() {
// TODO: In future if need added logic to get SP X509 cert for Idps that need signed requests
this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key());
this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key());
this.spSingleSignOnUrl = _configDao.getValue(Config.SAMLServiceProviderSingleSignOnURL.key());
this.spSingleLogOutUrl = _configDao.getValue(Config.SAMLServiceProviderSingleLogOutURL.key());
String idpMetaDataUrl = _configDao.getValue(Config.SAMLIdentityProviderMetadataURL.key());
int tolerance = 30000;
String timeout = _configDao.getValue(Config.SAMLTimeout.key());
if (timeout != null) {
tolerance = Integer.parseInt(timeout);
}
try {
DefaultBootstrap.bootstrap();
idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance);
idpMetaDataProvider.setRequireValidMetadata(true);
idpMetaDataProvider.setParserPool(new BasicParserPool());
idpMetaDataProvider.initialize();
EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor(this.identityProviderId);
IDPSSODescriptor idpssoDescriptor = idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
if (idpssoDescriptor != null) {
for (SingleSignOnService ssos: idpssoDescriptor.getSingleSignOnServices()) {
if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
this.idpSingleSignOnUrl = ssos.getLocation();
}
}
for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) {
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
this.idpSingleLogOutUrl = slos.getLocation();
}
}
for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) {
if (kd.getUse() == UsageType.SIGNING) {
try {
this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
} catch (CertificateException ignored) {
}
}
if (kd.getUse() == UsageType.ENCRYPTION) {
try {
this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
} catch (CertificateException ignored) {
}
}
}
} else {
s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work");
}
} catch (MetadataProviderException e) {
s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage());
s_logger.error("SAML2 Authentication may be unavailable");
} catch (ConfigurationException | FactoryConfigurationError e) {
s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage());
}
if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) {
s_logger.error("SAML based authentication won't work");
}
return true;
}
@Override
public List<Class<?>> getAuthCommands() {
if (!isSAMLPluginEnabled()) {
return null;
}
List<Class<?>> cmdList = new ArrayList<Class<?>>();
cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
cmdList.add(GetServiceProviderMetaDataCmd.class);
return cmdList;
}
public String getServiceProviderId() {
return serviceProviderId;
}
public String getIdpSingleSignOnUrl() {
return this.idpSingleSignOnUrl;
}
public String getIdpSingleLogOutUrl() {
return this.idpSingleLogOutUrl;
}
public String getSpSingleSignOnUrl() {
return spSingleSignOnUrl;
}
public String getSpSingleLogOutUrl() {
return spSingleLogOutUrl;
}
public String getIdentityProviderId() {
return identityProviderId;
}
public X509Certificate getIdpSigningKey() {
return idpSigningKey;
}
public X509Certificate getIdpEncryptionKey() {
return idpEncryptionKey;
}
public Boolean isSAMLPluginEnabled() {
return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key()));
}
}

View File

@ -12,7 +12,7 @@
// 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;
package org.apache.cloudstack.saml;
import com.cloud.server.auth.DefaultUserAuthenticator;
import com.cloud.server.auth.UserAuthenticator;
@ -21,6 +21,7 @@ import com.cloud.user.UserAccount;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.Pair;
import org.apache.cloudstack.utils.auth.SAMLUtils;
import org.apache.log4j.Logger;
import javax.ejb.Local;
@ -47,8 +48,8 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
} else {
User user = _userDao.getUser(userAccount.getId());
// TODO: check SAMLRequest, signature etc. from requestParameters
if (user != null && user.getUuid().startsWith("saml")) {
if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid()) &&
requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}
@ -58,8 +59,6 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
@Override
public String encode(final String password) {
// TODO: Complete method
StringBuilder sb = new StringBuilder(32);
return sb.toString();
return SAMLUtils.generateSecureRandomId();
}
}

View File

@ -19,21 +19,68 @@
package org.apache.cloudstack;
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
import com.cloud.user.UserAccountVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.Pair;
import org.apache.cloudstack.saml.SAML2UserAuthenticator;
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 java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class SAML2UserAuthenticatorTest {
@Mock
UserAccountDao userAccountDao;
@Mock
UserDao userDao;
@Test
public void encode() {
Assert.assertTrue(new SAML2UserAuthenticator().encode("random String").length() == 32);
}
@Test
public void authenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
SAML2UserAuthenticator authenticator = new SAML2UserAuthenticator();
Field daoField = SAML2UserAuthenticator.class.getDeclaredField("_userAccountDao");
daoField.setAccessible(true);
daoField.set(authenticator, userAccountDao);
Field userDaoField = SAML2UserAuthenticator.class.getDeclaredField("_userDao");
userDaoField.setAccessible(true);
userDaoField.set(authenticator, userDao);
UserAccountVO account = new UserAccountVO();
account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99");
account.setId(1L);
UserVO user = new UserVO();
user.setUuid(SAMLUtils.createSAMLId("someUID"));
Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
Mockito.when(userDao.getUser(Mockito.anyLong())).thenReturn(user);
// When there is no SAMLRequest in params
Pair<Boolean, ActionOnFailedAuthentication> pair1 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, null);
Assert.assertFalse(pair1.first());
// When there is SAMLRequest in params
Map<String, Object[]> params = new HashMap<String, Object[]>();
params.put(SAMLUtils.SAML_RESPONSE, new Object[]{});
Pair<Boolean, ActionOnFailedAuthentication> pair2 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, params);
Assert.assertTrue(pair2.first());
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.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;
@RunWith(MockitoJUnitRunner.class)
public class GetServiceProviderMetaDataCmdTest {
@Mock
ApiServerService apiServer;
@Mock
SAML2AuthManager samlAuthManager;
@Mock
HttpSession session;
@Mock
HttpServletResponse resp;
@Test
public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
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();
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, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), 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);
}
}

View File

@ -0,0 +1,175 @@
package org.apache.cloudstack.api.command;
import com.cloud.domain.Domain;
import com.cloud.user.AccountService;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.db.EntityManager;
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.joda.time.DateTime;
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 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.NameID;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.Status;
import org.opensaml.saml2.core.StatusCode;
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.NameIDBuilder;
import org.opensaml.saml2.core.impl.ResponseBuilder;
import org.opensaml.saml2.core.impl.StatusBuilder;
import org.opensaml.saml2.core.impl.StatusCodeBuilder;
import org.opensaml.saml2.core.impl.SubjectBuilder;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class SAML2LoginAPIAuthenticatorCmdTest {
@Mock
ApiServerService apiServer;
@Mock
SAML2AuthManager samlAuthManager;
@Mock
ConfigurationDao configDao;
@Mock
EntityManager entityMgr;
@Mock
DomainManager domainMgr;
@Mock
AccountService accountService;
@Mock
Domain domain;
@Mock
HttpSession session;
@Mock
HttpServletResponse resp;
private Response buildMockResponse() throws Exception {
Response samlMessage = new ResponseBuilder().buildObject();
samlMessage.setID("foo");
samlMessage.setVersion(SAMLVersion.VERSION_20);
samlMessage.setIssueInstant(new DateTime(0));
Status status = new StatusBuilder().buildObject();
StatusCode statusCode = new StatusCodeBuilder().buildObject();
statusCode.setValue(StatusCode.SUCCESS_URI);
status.setStatusCode(statusCode);
samlMessage.setStatus(status);
Assertion assertion = new AssertionBuilder().buildObject();
Subject subject = new SubjectBuilder().buildObject();
NameID nameID = new NameIDBuilder().buildObject();
nameID.setValue("SOME-UNIQUE-ID");
nameID.setFormat(NameIDType.PERSISTENT);
subject.setNameID(nameID);
assertion.setSubject(subject);
AuthnStatement authnStatement = new AuthnStatementBuilder().buildObject();
authnStatement.setSessionIndex("Some Session String");
assertion.getAuthnStatements().add(authnStatement);
AttributeStatement attributeStatement = new AttributeStatementBuilder().buildObject();
assertion.getAttributeStatements().add(attributeStatement);
samlMessage.getAssertions().add(assertion);
return samlMessage;
}
@Test
public void testAuthenticate() throws Exception {
SAML2LoginAPIAuthenticatorCmd cmd = Mockito.spy(new SAML2LoginAPIAuthenticatorCmd());
Field apiServerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
apiServerField.setAccessible(true);
apiServerField.set(cmd, apiServer);
Field managerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
managerField.setAccessible(true);
managerField.set(cmd, samlAuthManager);
Field accountServiceField = BaseCmd.class.getDeclaredField("_accountService");
accountServiceField.setAccessible(true);
accountServiceField.set(cmd, accountService);
Field entityMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_entityMgr");
entityMgrField.setAccessible(true);
entityMgrField.set(cmd, entityMgr);
Field domainMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_domainMgr");
domainMgrField.setAccessible(true);
domainMgrField.set(cmd, domainMgr);
Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao");
configDaoField.setAccessible(true);
configDaoField.set(cmd, configDao);
String spId = "someSPID";
String url = "someUrl";
X509Certificate cert = SAMLUtils.generateRandomX509Certificate();
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null);
Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url);
Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url);
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);
UserVO user = new UserVO();
user.setUuid(SAMLUtils.createSAMLId("someUID"));
Mockito.when(entityMgr.findByUuid(Mockito.eq(User.class), Mockito.anyString())).thenReturn((User) user);
Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false);
Map<String, Object[]> params = new HashMap<String, Object[]>();
// SSO redirection test
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
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"});
Mockito.stub(cmd.processSAMLResponse(Mockito.anyString())).toReturn(buildMockResponse());
try {
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
} catch (ServerApiException ignored) {
}
Mockito.verify(configDao, Mockito.atLeastOnce()).getValue(Mockito.anyString());
Mockito.verify(domainMgr, Mockito.times(1)).getDomain(Mockito.anyString());
Mockito.verify(entityMgr, Mockito.times(1)).findByUuid(Mockito.eq(User.class), Mockito.anyString());
Mockito.verify(apiServer, Mockito.times(1)).verifyUser(Mockito.anyLong());
}
@Test
public void testGetAPIType() {
Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.framework.config.dao.ConfigurationDao;
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.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.security.cert.X509Certificate;
@RunWith(MockitoJUnitRunner.class)
public class SAML2LogoutAPIAuthenticatorCmdTest {
@Mock
ApiServerService apiServer;
@Mock
SAML2AuthManager samlAuthManager;
@Mock
ConfigurationDao configDao;
@Mock
HttpSession session;
@Mock
HttpServletResponse resp;
@Test
public void testAuthenticate() throws Exception {
SAML2LogoutAPIAuthenticatorCmd cmd = new SAML2LogoutAPIAuthenticatorCmd();
Field apiServerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
apiServerField.setAccessible(true);
apiServerField.set(cmd, apiServer);
Field managerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
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();
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, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());
Mockito.verify(session, Mockito.atLeastOnce()).getAttribute(Mockito.anyString());
}
@Test
public void testGetAPIType() throws Exception {
Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGOUT_API);
}
}

View File

@ -94,6 +94,7 @@
<cs.mycila.license.version>2.5</cs.mycila.license.version>
<cs.findbugs.version>2.5.3</cs.findbugs.version>
<cs.javadoc.version>2.9.1</cs.javadoc.version>
<cs.opensaml.version>2.6.1</cs.opensaml.version>
</properties>
<distributionManagement>

View File

@ -133,6 +133,11 @@
<artifactId>cloud-engine-components-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>${cs.opensaml.version}</version>
</dependency>
</dependencies>
<build>
<testResources>

View File

@ -32,7 +32,10 @@
http://www.springframework.org/schema/util/spring-util-3.0.xsd"
>
<bean id="authenticationManagerImpl" class="com.cloud.api.auth.APIAuthenticationManagerImpl" />
<bean id="authenticationManagerImpl" class="com.cloud.api.auth.APIAuthenticationManagerImpl">
<property name="apiAuthenticators"
value="#{pluggableAPIAuthenticatorsRegistry.registered}" />
</bean>
<bean id="accountManagerImpl" class="com.cloud.user.AccountManagerImpl">
<property name="userAuthenticators"

View File

@ -16,80 +16,49 @@
// under the License.
package com.cloud.api;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.cloud.api.dispatch.DispatchChainFactory;
import com.cloud.api.dispatch.DispatchTask;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.configuration.Config;
import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventCategory;
import com.cloud.event.EventTypes;
import com.cloud.exception.AccountLimitException;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.RequestLimitException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.UserVO;
import com.cloud.utils.HttpUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpServerConnection;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.SocketHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Component;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.UUIDManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.exception.ExceptionProxyObject;
import org.apache.cloudstack.acl.APIChecker;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.BaseAsyncCreateCmd;
import org.apache.cloudstack.api.BaseCmd;
@ -122,6 +91,7 @@ import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.CreateCmdResponse;
import org.apache.cloudstack.api.response.ExceptionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.LoginCmdResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
@ -134,44 +104,74 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.MessageDispatcher;
import org.apache.cloudstack.framework.messagebus.MessageHandler;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpServerConnection;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.SocketHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Component;
import com.cloud.api.dispatch.DispatchChainFactory;
import com.cloud.api.dispatch.DispatchTask;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.configuration.Config;
import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventCategory;
import com.cloud.event.EventTypes;
import com.cloud.exception.AccountLimitException;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.RequestLimitException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.UserVO;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.UUIDManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.exception.ExceptionProxyObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService {
@ -932,8 +932,55 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
return null;
}
private ResponseObject createLoginResponse(HttpSession session) {
LoginCmdResponse response = new LoginCmdResponse();
response.setTimeout(session.getMaxInactiveInterval());
final String user_UUID = (String)session.getAttribute("user_UUID");
session.removeAttribute("user_UUID");
response.setUserId(user_UUID);
final String domain_UUID = (String)session.getAttribute("domain_UUID");
session.removeAttribute("domain_UUID");
response.setDomainId(domain_UUID);
final Enumeration attrNames = session.getAttributeNames();
if (attrNames != null) {
while (attrNames.hasMoreElements()) {
final String attrName = (String) attrNames.nextElement();
final Object attrObj = session.getAttribute(attrName);
if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) {
response.setUsername(attrObj.toString());
}
if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) {
response.setAccount(attrObj.toString());
}
if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) {
response.setFirstName(attrObj.toString());
}
if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) {
response.setLastName(attrObj.toString());
}
if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) {
response.setType((attrObj.toString()));
}
if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) {
response.setTimeZone(attrObj.toString());
}
if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) {
response.setRegistered(attrObj.toString());
}
if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) {
response.setSessionKey(attrObj.toString());
}
}
}
response.setResponseName("loginresponse");
return response;
}
@Override
public void loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress,
public ResponseObject loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress,
final Map<String, Object[]> requestParameters) throws CloudAuthenticationException {
// We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist
// we will default to ROOT
@ -1003,7 +1050,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
final String sessionKey = Base64.encodeBase64String(sessionKeyBytes);
session.setAttribute("sessionkey", sessionKey);
return;
return createLoginResponse(session);
}
throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials");
}

View File

@ -16,9 +16,9 @@
// under the License.
package com.cloud.api;
import com.cloud.api.auth.APIAuthenticationManager;
import com.cloud.api.auth.APIAuthenticationType;
import com.cloud.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
@ -26,6 +26,7 @@ import com.cloud.utils.HttpUtils;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.EntityManager;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;

View File

@ -19,6 +19,9 @@ package com.cloud.api.auth;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.log4j.Logger;
import javax.ejb.Local;
@ -32,12 +35,21 @@ import java.util.Map;
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
public static final Logger s_logger = Logger.getLogger(APIAuthenticationManagerImpl.class.getName());
private List<PluggableAPIAuthenticator> _apiAuthenticators;
private static Map<String, Class<?>> s_authenticators = null;
private static List<Class<?>> s_commandList = null;
public APIAuthenticationManagerImpl() {
}
public List<PluggableAPIAuthenticator> getApiAuthenticators() {
return _apiAuthenticators;
}
public void setApiAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
_apiAuthenticators = authenticators;
}
@Override
public boolean start() {
s_authenticators = new HashMap<String, Class<?>>();
@ -53,12 +65,13 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
@Override
public List<Class<?>> getCommands() {
if (s_commandList == null) {
s_commandList = new ArrayList<Class<?>>();
s_commandList.add(DefaultLoginAPIAuthenticatorCmd.class);
s_commandList.add(DefaultLogoutAPIAuthenticatorCmd.class);
List<Class<?>> cmdList = new ArrayList<Class<?>>();
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) {
cmdList.addAll(apiAuthenticator.getAuthCommands());
}
return s_commandList;
return cmdList;
}
@Override
@ -68,6 +81,7 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
try {
apiAuthenticator = (APIAuthenticator) s_authenticators.get(name).newInstance();
apiAuthenticator = ComponentContext.inject(apiAuthenticator);
apiAuthenticator.setAuthenticators(_apiAuthenticators);
} catch (InstantiationException | IllegalAccessException e) {
if (s_logger.isDebugEnabled()) {
s_logger.debug("APIAuthenticationManagerImpl::getAPIAuthenticator failed: " + e.getMessage());

View File

@ -16,7 +16,7 @@
// under the License.
package com.cloud.api.auth;
import com.cloud.api.ApiServerService;
import org.apache.cloudstack.api.ApiServerService;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
@ -25,15 +25,17 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.LoginCmdResponse;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
@APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
@ -100,54 +102,6 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
}
private String createLoginResponse(HttpSession session, String responseType) {
LoginCmdResponse response = new LoginCmdResponse();
response.setTimeout(session.getMaxInactiveInterval());
final String user_UUID = (String)session.getAttribute("user_UUID");
session.removeAttribute("user_UUID");
response.setUserId(user_UUID);
final String domain_UUID = (String)session.getAttribute("domain_UUID");
session.removeAttribute("domain_UUID");
response.setDomainId(domain_UUID);
// FIXME: the while loop mess
final Enumeration attrNames = session.getAttributeNames();
if (attrNames != null) {
while (attrNames.hasMoreElements()) {
final String attrName = (String) attrNames.nextElement();
final Object attrObj = session.getAttribute(attrName);
if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) {
response.setUsername(attrObj.toString());
}
if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) {
response.setAccount(attrObj.toString());
}
if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) {
response.setFirstName(attrObj.toString());
}
if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) {
response.setLastName(attrObj.toString());
}
if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) {
response.setType((attrObj.toString()));
}
if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) {
response.setTimeZone(attrObj.toString());
}
if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) {
response.setRegistered(attrObj.toString());
}
if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) {
response.setSessionKey(attrObj.toString());
}
}
}
response.setResponseName(getCommandName());
return ApiResponseSerializer.toSerializedString((ResponseObject) response, responseType);
}
@Override
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
@ -197,10 +151,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
if (username != null) {
final String pwd = ((password == null) ? null : password[0]);
try {
_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params);
auditTrailSb.insert(0, "(userId=" + session.getAttribute("userid") + " accountId=" + ((Account) session.getAttribute("accountobj")).getId() +
" sessionId=" + session.getId() + ")");
return createLoginResponse(session, responseType);
return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params),
responseType);
} catch (final CloudAuthenticationException ex) {
// TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401)
try {
@ -222,4 +174,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGIN_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
}
}

View File

@ -22,11 +22,15 @@ import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.LogoutCmdResponse;
import org.apache.log4j.Logger;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Map;
@APICommand(name = "logout", description = "Logs out the user", responseObject = LogoutCmdResponse.class, entityType = {})
@ -68,4 +72,8 @@ public class DefaultLogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuth
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGOUT_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
}
}

View File

@ -1379,6 +1379,86 @@ 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),
SAMLUserAccountName(
"Advanced",
ManagementServer.class,
String.class,
"saml2.default.accountname",
"admin",
"The name of the default account to use when creating users from SAML SSO",
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", "128", "Default RAM for router VM (in MB).", null),

View File

@ -16,9 +16,9 @@
// under the License.
package com.cloud.api;
import com.cloud.api.auth.APIAuthenticationManager;
import com.cloud.api.auth.APIAuthenticationType;
import com.cloud.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import com.cloud.server.ManagementServer;
import com.cloud.user.Account;
import com.cloud.user.AccountService;

View File

@ -114,6 +114,7 @@ known_categories = {
'login': 'Authentication',
'logout': 'Authentication',
'saml': 'Authentication',
'getSPMetadata': 'Authentication',
'Capacity': 'System Capacity',
'NetworkDevice': 'Network Device',
'ExternalLoadBalancer': 'Ext Load Balancer',

View File

@ -129,17 +129,25 @@
i.e. calling listCapabilities API with g_sessionKey from $.cookie('sessionKey') will succeed,
then userValid will be set to true, then an user object (instead of "false") will be returned, then login screen will be bypassed.
*/
var unBoxCookieValue = function (cookieName) {
var cookieValue = $.cookie(cookieName);
if (cookieValue && cookieValue.length > 2 && cookieValue[0] === '"' && cookieValue[cookieValue.length-1] === '"') {
cookieValue = cookieValue.slice(1, cookieValue.length-1);
$.cookie(cookieName, cookieValue, { expires: 1 });
}
return cookieValue;
};
g_mySession = $.cookie('JSESSIONID');
g_sessionKey = $.cookie('sessionKey');
g_role = $.cookie('role');
g_username = $.cookie('username');
g_userid = $.cookie('userid');
g_account = $.cookie('account');
g_domainid = $.cookie('domainid');
g_userfullname = $.cookie('userfullname');
g_timezone = $.cookie('timezone');
g_sessionKey = unBoxCookieValue('sessionKey');
g_role = unBoxCookieValue('role');
g_userid = unBoxCookieValue('userid');
g_domainid = unBoxCookieValue('domainid');
g_account = unBoxCookieValue('account');
g_username = unBoxCookieValue('username');
g_userfullname = unBoxCookieValue('userfullname');
g_timezone = unBoxCookieValue('timezone');
if ($.cookie('timezoneoffset') != null)
g_timezoneoffset = isNaN($.cookie('timezoneoffset')) ? null : parseFloat($.cookie('timezoneoffset'));
g_timezoneoffset = isNaN(unBoxCookieValue('timezoneoffset')) ? null : parseFloat(unBoxCookieValue('timezoneoffset'));
else
g_timezoneoffset = null;
} else { //single-sign-on (bypass login screen)

View File

@ -143,6 +143,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>${cs.opensaml.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>

View File

@ -0,0 +1,232 @@
//
// 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.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.joda.time.DateTime;
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.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.math.BigInteger;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Date;
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 CERTIFICATE_NAME = "SAMLSP_X509CERTIFICATE";
public static String createSAMLId(String uid) {
return SAML_NS + uid;
}
public static Boolean checkSAMLUserId(String uuid) {
return uuid.startsWith(SAML_NS);
}
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.MINIMUM);
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 X509Certificate generateRandomX509Certificate() throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException {
Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
Date validityEndDate = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000);
Security.addProvider(new BouncyCastleProvider());
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(1024, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
X500Principal dnName = new X500Principal("CN=Apache CloudStack");
X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
certGen.setSubjectDN(dnName);
certGen.setIssuerDN(dnName);
certGen.setNotBefore(validityBeginDate);
certGen.setNotAfter(validityEndDate);
certGen.setPublicKey(keyPair.getPublic());
certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
return certGen.generate(keyPair.getPrivate(), "BC");
}
}

View File

@ -0,0 +1,67 @@
//
// 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;
public class SAMLUtilsTest extends TestCase {
@Test
public void testSAMLId() throws Exception {
assertTrue(SAMLUtils.checkSAMLUserId(SAMLUtils.createSAMLId("someUID")));
assertFalse(SAMLUtils.checkSAMLUserId("randomUID"));
}
@Test
public void testGenerateSecureRandomId() throws Exception {
assertTrue(SAMLUtils.generateSecureRandomId().length() == 32);
}
@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);
}
}