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/a1dc9e81 Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/a1dc9e81 Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/a1dc9e81 Branch: refs/heads/master Commit: a1dc9e8189ebdab3f7e8b849f1777f282a7a295b Parents: 9c7204d Author: Rohit Yadav <rohit.ya...@shapeblue.com> Authored: Mon Aug 18 03:43:58 2014 +0200 Committer: Rohit Yadav <rohit.ya...@shapeblue.com> Committed: Thu Aug 28 19:45:21 2014 +0200 ---------------------------------------------------------------------- .../api/auth/SAML2LoginAPIAuthenticatorCmd.java | 289 ++++++++++--------- 1 file changed, 153 insertions(+), 136 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1dc9e81/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