SAML: WIP redirections work now 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/a05c234e Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/a05c234e Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/a05c234e Branch: refs/heads/saml2 Commit: a05c234e54ea127d14f875042c46c05a05e35138 Parents: 37a713f Author: Rohit Yadav <rohit.ya...@shapeblue.com> Authored: Sun Aug 17 19:12:00 2014 +0200 Committer: Rohit Yadav <rohit.ya...@shapeblue.com> Committed: Sat Aug 23 20:34:39 2014 +0200 ---------------------------------------------------------------------- .../api/auth/SAML2LoginAPIAuthenticatorCmd.java | 241 ++++++++++++++++++- 1 file changed, 239 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a05c234e/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 beba4f1..c6b0bb6 100644 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java @@ -26,11 +26,54 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.LoginCmdResponse; 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.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameIDPolicy; +import org.opensaml.saml2.core.RequestedAuthnContext; +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.NameIDPolicyBuilder; +import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; +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.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; 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 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.SecureRandom; import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; @APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { @@ -71,12 +114,206 @@ 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) { + 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 = ""; + + try { + DefaultBootstrap.bootstrap(); + AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, identityProviderUrl, resourceUrl); // SAML AuthRequest + encodedAuthRequest = encodeAuthnRequest(authnRequest); + } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { + s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); + } + return identityProviderUrl + "?SAMLRequest=" + encodedAuthRequest; // + "&RelayState=" + relayState; + } + + private AuthnRequest buildAuthnRequestObject(String authnId, String idpUrl, String consumerUrl) { + // Issuer object + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + //SAMLConstants.SAML20_NS, + // "Issuer", "samlp"); + issuer.setValue("apache-cloudstack"); + + // NameIDPolicy + NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); + NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"); + nameIdPolicy.setSPNameQualifier("Apache CloudStack"); + nameIdPolicy.setAllowCreate(true); + + // AuthnContextClass + AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); + AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( + SAMLConstants.SAML20_NS, + "AuthnContextClassRef", "saml"); + authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + + // AuthnContex + RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); + RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); + requestedAuthnContext + .setComparison(AuthnContextComparisonTypeEnumeration.EXACT); + requestedAuthnContext.getAuthnContextClassRefs().add( + authnContextClassRef); + + + // Creation of AuthRequestObject + AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); + AuthnRequest authnRequest = authRequestBuilder.buildObject(); + //SAMLConstants.SAML20P_NS, + // "AuthnRequest", "samlp"); + authnRequest.setID(authnId); + authnRequest.setDestination(idpUrl); + authnRequest.setVersion(SAMLVersion.VERSION_20); + authnRequest.setForceAuthn(true); + authnRequest.setIsPassive(false); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(new DateTime()); + authnRequest.setProviderName("Apache CloudStack"); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setAssertionConsumerServiceURL(consumerUrl); + //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(); + 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 + deflaterOutputStream.close(); + + encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream + .toByteArray(), Base64.DONT_BREAK_LINES); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, + "UTF-8").trim(); // encoding string + + return encodedRequestMessage; + } + + + public String processResponseMessage(String responseMessage) { + + XMLObject responseObject = null; + + try { + + responseObject = this.unmarshall(responseMessage); + + } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + e.printStackTrace(); + } + + return this.getResult(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(); + + 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; + } + + + @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; try { - resp.sendRedirect(getIdpUrl()); + 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