You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2014/08/18 03:46:57 UTC
[5/5] git commit: updated refs/heads/saml2 to 66129c3
SAML2LoginAPIAuthenticatorCmd: Implement SAML SSO using HTTP Redirect binding
- Creates SAMLRequest and uses HTTP redirect binding (uses GET/302)
- Redirects to IdP for auth
- On successful auth, check for assertion
- Tries to get attributes based on standard LDAP attribute names
- Next, gets user using EntityManager, if not found creates one with NameID as UUID
- Finally tries to log in and redirect
Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/66129c3f
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/66129c3f
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/66129c3f
Branch: refs/heads/saml2
Commit: 66129c3f4beae3c277b8469be1ae80c2c25bd6e0
Parents: ade9b08
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Mon Aug 18 03:43:58 2014 +0200
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Mon Aug 18 03:43:58 2014 +0200
----------------------------------------------------------------------
.../api/auth/SAML2LoginAPIAuthenticatorCmd.java | 289 ++++++++++---------
1 file changed, 153 insertions(+), 136 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/66129c3f/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java
index c6b0bb6..4e17d3d 100644
--- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java
+++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java
@@ -17,7 +17,13 @@
package com.cloud.api.auth;
+import com.cloud.api.ApiServerService;
+import com.cloud.api.response.ApiResponseSerializer;
+import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
+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;
@@ -25,18 +31,26 @@ import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.LoginCmdResponse;
+import org.apache.cloudstack.context.CallContext;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.SAMLVersion;
import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.Attribute;
+import org.opensaml.saml2.core.AttributeStatement;
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.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.StatusCode;
import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml2.core.impl.IssuerBuilder;
@@ -49,15 +63,15 @@ import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallerFactory;
import org.opensaml.xml.io.UnmarshallingException;
+import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.util.Base64;
import org.opensaml.xml.util.XMLHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
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.DocumentBuilder;
@@ -71,6 +85,7 @@ import java.io.StringWriter;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.SecureRandom;
+import java.util.List;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
@@ -86,6 +101,11 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
@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;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -114,34 +134,30 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
}
- public String buildAuthnRequestUrl(String resourceUrl) {
+ public String buildAuthnRequestUrl(String consumerUrl, String identityProviderUrl) {
String randomId = new BigInteger(130, new SecureRandom()).toString(32);
- // TODO: Add method to get this url from metadata
- String identityProviderUrl = "https://idp.ssocircle.com:443/sso/SSORedirect/metaAlias/ssocircle";
- String encodedAuthRequest = "";
-
+ String spId = "org.apache.cloudstack";
+ String redirectUrl = "";
try {
DefaultBootstrap.bootstrap();
- AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, identityProviderUrl, resourceUrl); // SAML AuthRequest
- encodedAuthRequest = encodeAuthnRequest(authnRequest);
+ AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl);
+ redirectUrl = identityProviderUrl + "?SAMLRequest=" + encodeAuthnRequest(authnRequest);
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) {
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
}
- return identityProviderUrl + "?SAMLRequest=" + encodedAuthRequest; // + "&RelayState=" + relayState;
+ return redirectUrl;
}
- private AuthnRequest buildAuthnRequestObject(String authnId, String idpUrl, String consumerUrl) {
+ private AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) {
// Issuer object
IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject();
- //SAMLConstants.SAML20_NS,
- // "Issuer", "samlp");
- issuer.setValue("apache-cloudstack");
+ issuer.setValue(spId);
// NameIDPolicy
NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
- nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
+ nameIdPolicy.setFormat(NameIDType.PERSISTENT);
nameIdPolicy.setSPNameQualifier("Apache CloudStack");
nameIdPolicy.setAllowCreate(true);
@@ -156,16 +172,13 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
requestedAuthnContext
- .setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
+ .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM);
requestedAuthnContext.getAuthnContextClassRefs().add(
authnContextClassRef);
-
// Creation of AuthRequestObject
AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
AuthnRequest authnRequest = authRequestBuilder.buildObject();
- //SAMLConstants.SAML20P_NS,
- // "AuthnRequest", "samlp");
authnRequest.setID(authnId);
authnRequest.setDestination(idpUrl);
authnRequest.setVersion(SAMLVersion.VERSION_20);
@@ -174,154 +187,158 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
authnRequest.setIssuer(issuer);
authnRequest.setIssueInstant(new DateTime());
authnRequest.setProviderName("Apache CloudStack");
- authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); //SAML2_ARTIFACT_BINDING_URI);
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
- //authnRequest.setNameIDPolicy(nameIdPolicy);
- //authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+ authnRequest.setNameIDPolicy(nameIdPolicy);
+ authnRequest.setRequestedAuthnContext(requestedAuthnContext);
return authnRequest;
}
private String encodeAuthnRequest(AuthnRequest authnRequest)
throws MarshallingException, IOException {
-
- Marshaller marshaller = null;
- org.w3c.dom.Element authDOM = null;
- StringWriter requestWriter = null;
- String requestMessage = null;
- Deflater deflater = null;
- ByteArrayOutputStream byteArrayOutputStream = null;
- DeflaterOutputStream deflaterOutputStream = null;
- String encodedRequestMessage = null;
-
- marshaller = org.opensaml.Configuration.getMarshallerFactory()
- .getMarshaller(authnRequest); // object to DOM converter
-
- authDOM = marshaller.marshall(authnRequest); // converting to a DOM
-
- requestWriter = new StringWriter();
+ Marshaller marshaller = Configuration.getMarshallerFactory()
+ .getMarshaller(authnRequest);
+ Element authDOM = marshaller.marshall(authnRequest);
+ StringWriter requestWriter = new StringWriter();
XMLHelper.writeNode(authDOM, requestWriter);
- requestMessage = requestWriter.toString(); // DOM to string
-
- deflater = new Deflater(Deflater.DEFLATED, true);
- byteArrayOutputStream = new ByteArrayOutputStream();
- deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream,
- deflater);
- deflaterOutputStream.write(requestMessage.getBytes()); // compressing
+ 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();
-
- encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream
- .toByteArray(), Base64.DONT_BREAK_LINES);
- encodedRequestMessage = URLEncoder.encode(encodedRequestMessage,
- "UTF-8").trim(); // encoding string
-
+ String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
+ encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim();
return encodedRequestMessage;
}
-
- public String processResponseMessage(String responseMessage) {
-
+ public Response processSAMLResponse(String responseMessage) {
XMLObject responseObject = null;
-
try {
-
responseObject = this.unmarshall(responseMessage);
} catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
- e.printStackTrace();
+ s_logger.error("SAMLResponse processing error: " + e.getMessage());
}
-
- return this.getResult(responseObject);
+ return (Response) responseObject;
}
private XMLObject unmarshall(String responseMessage)
throws ConfigurationException, ParserConfigurationException,
SAXException, IOException, UnmarshallingException {
-
- DocumentBuilderFactory documentBuilderFactory = null;
- DocumentBuilder docBuilder = null;
- Document document = null;
- Element element = null;
- UnmarshallerFactory unmarshallerFactory = null;
- Unmarshaller unmarshaller = null;
-
- DefaultBootstrap.bootstrap();
-
- documentBuilderFactory = DocumentBuilderFactory.newInstance();
-
+ try {
+ DefaultBootstrap.bootstrap();
+ } catch (ConfigurationException | FactoryConfigurationError e) {
+ s_logger.error("SAML response message decoding error: " + e.getMessage());
+ }
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
-
- docBuilder = documentBuilderFactory.newDocumentBuilder();
-
- document = docBuilder.parse(new ByteArrayInputStream(responseMessage
- .trim().getBytes())); // response to DOM
-
- element = document.getDocumentElement(); // the DOM element
-
- unmarshallerFactory = Configuration.getUnmarshallerFactory();
-
- unmarshaller = unmarshallerFactory.getUnmarshaller(element);
-
- return unmarshaller.unmarshall(element); // Response object
-
- }
-
- private String getResult(XMLObject responseObject) {
-
- Element ele = null;
- NodeList statusNodeList = null;
- Node statusNode = null;
- NamedNodeMap statusAttr = null;
- Node valueAtt = null;
- String statusValue = null;
-
- String[] word = null;
- String result = null;
-
- NodeList nameIDNodeList = null;
- Node nameIDNode = null;
- String nameID = null;
-
- // reading the Response Object
- ele = responseObject.getDOM();
- statusNodeList = ele.getElementsByTagName("samlp:StatusCode");
- statusNode = statusNodeList.item(0);
- statusAttr = statusNode.getAttributes();
- valueAtt = statusAttr.item(0);
- statusValue = valueAtt.getNodeValue();
-
- word = statusValue.split(":");
- result = word[word.length - 1];
-
- nameIDNodeList = ele.getElementsByTagNameNS(
- "urn:oasis:names:tc:SAML:2.0:assertion", "NameID");
- nameIDNode = nameIDNodeList.item(0);
- nameID = nameIDNode.getFirstChild().getNodeValue();
-
- result = nameID + ":" + result;
-
- return result;
+ 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 unmarshaller.unmarshall(element);
}
-
-
@Override
- public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
- String response = null;
+ 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 {
- String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=login");
- resp.sendRedirect(redirectUrl);
-
- //resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
- //resp.setHeader("Location", redirectUrl);
-
- // TODO: create and send assertion with the URL as GET params
-
+ if (!params.containsKey("SAMLResponse")) {
+ final String[] idps = (String[])params.get("idpurl");
+ String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=samlsso", idps[0]);
+ resp.sendRedirect(redirectUrl);
+ return "";
+ } else {
+ final String samlResponse = ((String[])params.get("SAMLResponse"))[0];
+ Response processedSAMLResponse = 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));
+ }
+
+ Signature sig = processedSAMLResponse.getSignature();
+ //SignatureValidator validator = new SignatureValidator(credential);
+ //validator.validate(sig);
+
+ String uniqueUserId = null;
+ String accountName = "admin"; //GET from config, try, fail
+ Long domainId = 1L; // GET from config, try, fail
+ String username = null;
+ String password = "";
+ String firstName = "";
+ String lastName = "";
+ String timeZone = "";
+ String email = "";
+
+ Assertion assertion = processedSAMLResponse.getAssertions().get(0);
+ NameID nameId = assertion.getSubject().getNameID();
+
+ if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) {
+ username = nameId.getValue();
+ uniqueUserId = "saml-" + username;
+ if (nameId.getFormat().equals(NameIDType.EMAIL)) {
+ email = username;
+ }
+ }
+
+ String issuer = assertion.getIssuer().getValue();
+ String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI();
+ 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 = "saml-" + 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.sendRedirect("http://localhost:8080/client");
+ 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());
}
- return response;
+ 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