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 <rohit.ya...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/9479a191
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/9479a191
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/9479a191

Branch: refs/heads/saml2
Commit: 9479a1913d43d15d31406111627030b8dd7d3f07
Parents: e426f78
Author: Rohit Yadav <rohit.ya...@shapeblue.com>
Authored: Mon Aug 18 03:43:58 2014 +0200
Committer: Rohit Yadav <rohit.ya...@shapeblue.com>
Committed: Sat Aug 23 20:34:39 2014 +0200

----------------------------------------------------------------------
 .../api/auth/SAML2LoginAPIAuthenticatorCmd.java | 289 ++++++++++---------
 1 file changed, 153 insertions(+), 136 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/9479a191/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

Reply via email to