diff --git a/gamsaml20/pom.xml b/gamsaml20/pom.xml new file mode 100644 index 000000000..f5a9c64f8 --- /dev/null +++ b/gamsaml20/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + com.genexus + parent + ${revision}${changelist} + + + gamsaml20 + GAM Saml 2.0 EO + + + UTF-8 + + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + compile + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + compile + + + org.apache.santuario + xmlsec + 3.0.3 + + + commons-io + commons-io + 2.11.0 + compile + + + + + gamsaml20 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.1 + + + + test-jar + + + + + + + + + \ No newline at end of file diff --git a/gamsaml20/src/main/java/com/genexus/saml20/Binding.java b/gamsaml20/src/main/java/com/genexus/saml20/Binding.java new file mode 100644 index 000000000..b6b4b4402 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/Binding.java @@ -0,0 +1,29 @@ +package com.genexus.saml20; + +import com.genexus.saml20.utils.SamlAssertionUtils; + +@SuppressWarnings("unused") +public abstract class Binding { + + abstract void init(String input); + + static String login(SamlParms parms, String relayState) { + return ""; + } + + static String logout(SamlParms parms, String relayState) { + return ""; + } + + abstract boolean verifySignatures(SamlParms parms); + + abstract String getLoginAssertions(); + + abstract String getLoginAttribute(String name); + + abstract String getRoles(String name); + + abstract String getLogoutAssertions(); + + abstract boolean isLogout(); +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/PostBinding.java b/gamsaml20/src/main/java/com/genexus/saml20/PostBinding.java new file mode 100644 index 000000000..885f354bf --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/PostBinding.java @@ -0,0 +1,73 @@ +package com.genexus.saml20; + +import com.genexus.saml20.utils.DSig; +import com.genexus.saml20.utils.Encoding; +import com.genexus.saml20.utils.SamlAssertionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.Document; + +import java.text.MessageFormat; + +@SuppressWarnings("unused") +public class PostBinding extends Binding { + + private static final Logger logger = LogManager.getLogger(PostBinding.class); + + private Document xmlDoc; + + public PostBinding() { + logger.trace("PostBinding constructor"); + xmlDoc = null; + } + // EXTERNAL OBJECT PUBLIC METHODS - BEGIN + + + public void init(String xml) { + logger.trace("init"); + this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml); + logger.debug(MessageFormat.format("Init - XML IdP response: {0}", Encoding.documentToString(xmlDoc))); + } + + public static String login(SamlParms parms, String relayState) { + //not implemented yet + logger.error("login - NOT IMPLEMENTED"); + return ""; + } + + public static String logout(SamlParms parms, String relayState) { + //not implemented yet + logger.error("logout - NOT IMPLEMENTED"); + return ""; + } + + public boolean verifySignatures(SamlParms parms) { + return DSig.validateSignatures(this.xmlDoc, parms.getTrustCertPath(), parms.getTrustCertAlias(), parms.getTrustCertPass()); + } + + public String getLoginAssertions() { + logger.trace("getLoginAssertions"); + return SamlAssertionUtils.getLoginInfo(this.xmlDoc); + } + + public String getLogoutAssertions() { + logger.trace("getLogoutAssertions"); + return SamlAssertionUtils.getLogoutInfo(this.xmlDoc); + } + + public String getLoginAttribute(String name) { + logger.trace("getLoginAttribute"); + return SamlAssertionUtils.getLoginAttribute(this.xmlDoc, name).trim(); + } + + public String getRoles(String name) { + logger.debug("getRoles"); + return SamlAssertionUtils.getRoles(this.xmlDoc, name); + } + + public boolean isLogout(){ + return SamlAssertionUtils.isLogout(this.xmlDoc); + } + + // EXTERNAL OBJECT PUBLIC METHODS - END +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/RedirectBinding.java b/gamsaml20/src/main/java/com/genexus/saml20/RedirectBinding.java new file mode 100644 index 000000000..53c6ee927 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/RedirectBinding.java @@ -0,0 +1,200 @@ +package com.genexus.saml20; + +import com.genexus.saml20.utils.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.signers.RSADigestSigner; +import org.bouncycastle.util.encoders.Base64; +import org.w3c.dom.Document; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public class RedirectBinding extends Binding { + + private static final Logger logger = LogManager.getLogger(RedirectBinding.class); + + private Document xmlDoc; + private Map redirectMessage; + + // EXTERNAL OBJECT PUBLIC METHODS - BEGIN + + + public RedirectBinding() { + logger.trace("RedirectBinding constructor"); + } + + public void init(String queryString) { + logger.trace("init"); + logger.debug(MessageFormat.format("init - queryString : {0}", queryString)); + this.redirectMessage = parseRedirect(queryString); + String xml = Encoding.decodeAndInflateXmlParameter(this.redirectMessage.get("SAMLResponse")); + logger.debug("init - inflated xml: {0}", xml); + this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml); + logger.debug(MessageFormat.format("init - XML IdP response: {0}", Encoding.documentToString(xmlDoc))); + } + + + public static String login(SamlParms parms, String relayState) { + Document request = SamlAssertionUtils.createLoginRequest(parms.getId(), parms.getEndPointLocation(), parms.getAcs(), parms.getIdentityProviderEntityID(), parms.getPolicyFormat(), parms.getAuthnContext(), parms.getServiceProviderEntityID(), parms.getForceAuthn()); + return generateQuery(request, parms.getEndPointLocation(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState); + } + + public static String logout(SamlParms parms, String relayState) { + Document request = SamlAssertionUtils.createLogoutRequest(parms.getId(), parms.getServiceProviderEntityID(), parms.getNameID(), parms.getSessionIndex(), parms.getSingleLogoutEndpoint()); + return generateQuery(request, parms.getSingleLogoutEndpoint(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState); + } + + public boolean verifySignatures(SamlParms parms) { + logger.debug("verifySignatures"); + + try { + return verifySignature_internal(parms.getTrustCertPath(), parms.getTrustCertPass(), parms.getTrustCertAlias()); + } catch (Exception e) { + logger.error("verifySignature", e); + return false; + } + } + + public String getLogoutAssertions() { + logger.trace("getLogoutAssertions"); + return SamlAssertionUtils.getLogoutInfo(this.xmlDoc); + } + + public String getRelayState() { + logger.trace("getRelayState"); + try { + return this.redirectMessage.get("RelayState") == null ? "" : URLDecoder.decode(this.redirectMessage.get("RelayState"), StandardCharsets.UTF_8.name()); + } catch (Exception e) { + logger.error("getRelayState", e); + return ""; + } + } + + public String getLoginAssertions() { + //Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding + logger.error("getLoginAssertions - NOT IMPLEMENTED insecure SAML implementation"); + return ""; + } + + public String getRoles(String name) { + //Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding + logger.error("getRoles - NOT IMPLEMENTED insecure SAML implementation"); + return ""; + } + + public String getLoginAttribute(String name) { + //Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding + logger.error("getLoginAttribute - NOT IMPLEMENTED insecure SAML implementation"); + return ""; + } + + public boolean isLogout(){ + return SamlAssertionUtils.isLogout(this.xmlDoc); + } + + // EXTERNAL OBJECT PUBLIC METHODS - END + + private boolean verifySignature_internal(String certPath, String certPass, String certAlias) { + logger.trace("verifySignature_internal"); + + byte[] signature = Encoding.decodeParameter(this.redirectMessage.get("Signature")); + + String signedMessage; + if (this.redirectMessage.containsKey("RelayState")) { + signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse")); + signedMessage += MessageFormat.format("&RelayState={0}", this.redirectMessage.get("RelayState")); + signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg")); + } else { + signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse")); + signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg")); + } + + byte[] query = signedMessage.getBytes(StandardCharsets.UTF_8); + + X509Certificate cert = Keys.loadCertificate(certPath, certAlias, certPass); + + try (InputStream inputStream = new ByteArrayInputStream(query)) { + String sigalg = URLDecoder.decode(this.redirectMessage.get("SigAlg"), StandardCharsets.UTF_8.name()); + RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(Hash.getHashFromSigAlg(sigalg))); + setUpSigner(signer, inputStream, Keys.getAsymmetricKeyParameter(cert), false); + return signer.verifySignature(signature); + } catch (Exception e) { + logger.error("verifySignature_internal", e); + return false; + } + } + + private static Map parseRedirect(String request) { + logger.trace("parseRedirect"); + Map result = new HashMap<>(); + String[] redirect = request.split("&"); + + for (String s : redirect) { + String[] res = s.split("="); + result.put(res[0], res[1]); + } + return result; + } + + private static String generateQuery(Document request, String destination, String certPath, String certPass, String alias, String relayState) { + logger.trace("generateQuery"); + try { + String samlRequestParameter = Encoding.delfateAndEncodeXmlParameter(Encoding.documentToString(request)); + String relayStateParameter = URLEncoder.encode(relayState, StandardCharsets.UTF_8.name()); + Hash hash = Keys.isBase64(certPath) ? Hash.getHash(certPass.toUpperCase().trim()) : Hash.getHash(Keys.getHash(certPath, alias, certPass)); + + String sigAlgParameter = URLEncoder.encode(Hash.getSigAlg(hash), StandardCharsets.UTF_8.name()); + String query = MessageFormat.format("SAMLRequest={0}&RelayState={1}&SigAlg={2}", samlRequestParameter, relayStateParameter, sigAlgParameter); + String signatureParameter = URLEncoder.encode(signRequest_RedirectBinding(query, certPath, certPass, hash, alias), StandardCharsets.UTF_8.name()); + + query += MessageFormat.format("&Signature={0}", signatureParameter); + + logger.debug(MessageFormat.format("generateQuery - query: {0}", query)); + return MessageFormat.format("{0}?{1}", destination, query); + } catch (Exception e) { + logger.error("generateQuery", e); + return ""; + } + + } + + private static String signRequest_RedirectBinding(String query, String path, String password, Hash hash, String alias) { + logger.trace("signRequest_RedirectBinding"); + RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(hash)); + byte[] inputText = query.getBytes(StandardCharsets.UTF_8); + try (InputStream inputStream = new ByteArrayInputStream(inputText)) { + setUpSigner(signer, inputStream, Keys.loadPrivateKey(path, alias, password), true); + byte[] outputBytes = signer.generateSignature(); + return Base64.toBase64String(outputBytes); + } catch (Exception e) { + logger.error("signRequest_RedirectBinding", e); + return ""; + } + } + + private static void setUpSigner(Signer signer, InputStream input, AsymmetricKeyParameter asymmetricKeyParameter, + boolean toSign) { + logger.trace("setUpSigner"); + byte[] buffer = new byte[8192]; + int n; + try { + signer.init(toSign, asymmetricKeyParameter); + while ((n = input.read(buffer)) > 0) { + signer.update(buffer, 0, n); + } + } catch (Exception e) { + logger.error("setUpSigner", e); + } + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/SamlParms.java b/gamsaml20/src/main/java/com/genexus/saml20/SamlParms.java new file mode 100644 index 000000000..589de28b1 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/SamlParms.java @@ -0,0 +1,180 @@ +package com.genexus.saml20; + +@SuppressWarnings("unused") +public class SamlParms { + + private String id; + private String endPointLocation; //IdP Login URL + private String singleLogoutEndpoint; //IdP Logout URL + private String acs; + private String identityProviderEntityID; //issuer + private String certPath; + private String certPass; + private String certAlias; + private String policyFormat; + private String authnContext; + private String serviceProviderEntityID; //spName + private boolean forceAuthn; + private String nameID; + private String sessionIndex; + private String trustCertPath; + private String trustCertPass; + private String trustCertAlias; + + + public SamlParms() { + id = ""; + endPointLocation = ""; + singleLogoutEndpoint = ""; + acs = ""; + identityProviderEntityID = ""; + certPath = ""; + certPass = ""; + certAlias = ""; + policyFormat = ""; + authnContext = ""; + serviceProviderEntityID = ""; + forceAuthn = false; + nameID = ""; + sessionIndex = ""; + trustCertAlias = ""; + trustCertPass = ""; + trustCertPath = ""; + } + + public void setId(String value) { + id = value; + } + + public String getId() { + return id; + } + + public void setEndPointLocation(String value) { + endPointLocation = value; + } + + public String getEndPointLocation() { + return endPointLocation; + } + + public void setSingleLogoutEndpoint(String value) { + singleLogoutEndpoint = value; + } + + public String getSingleLogoutEndpoint() { + return singleLogoutEndpoint; + } + + public void setAcs(String value) { + acs = value; + } + + public String getAcs() { + return acs; + } + + public void setIdentityProviderEntityID(String value) { + identityProviderEntityID = value; + } + + public String getIdentityProviderEntityID() { + return identityProviderEntityID; + } + + public void setCertPath(String value) { + certPath = value; + } + + public String getCertPath() { + return certPath; + } + + public void setCertPass(String value) { + certPass = value; + } + + public String getCertPass() { + return certPass; + } + + public void setCertAlias(String value) { + certAlias = value; + } + + public String getCertAlias() { + return certAlias; + } + + public void setPolicyFormat(String value) { + policyFormat = value; + } + + public String getPolicyFormat() { + return policyFormat; + } + + public void setAuthnContext(String value) { + authnContext = value; + } + + public String getAuthnContext() { + return authnContext; + } + + public void setServiceProviderEntityID(String value) { + serviceProviderEntityID = value; + } + + public String getServiceProviderEntityID() { + return serviceProviderEntityID; + } + + public void setForceAuthn(boolean value) { + forceAuthn = value; + } + + public boolean getForceAuthn() { + return forceAuthn; + } + + public void setNameID(String value) { + nameID = value; + } + + public String getNameID() { + return nameID; + } + + public void setSessionIndex(String value) { + sessionIndex = value; + } + + public String getSessionIndex() { + return sessionIndex; + } + + public void setTrustCertPath(String value) { + trustCertPath = value; + } + + public String getTrustCertPath() { + return trustCertPath; + } + + public void setTrustCertPass(String value) { + trustCertPass = value; + } + + public String getTrustCertPass() { + return trustCertPass; + } + + public void setTrustCertAlias(String value) { + trustCertAlias = value; + } + + public String getTrustCertAlias() { + return trustCertAlias; + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/DSig.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/DSig.java new file mode 100644 index 000000000..e3aa3a623 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/DSig.java @@ -0,0 +1,107 @@ +package com.genexus.saml20.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.utils.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DSig { + + private static final Logger logger = LogManager.getLogger(DSig.class); + + public static boolean validateSignatures(Document xmlDoc, String certPath, String certAlias, String certPassword) { + logger.trace("validateSignatures"); + X509Certificate cert = Keys.loadCertificate(certPath, certAlias, certPassword); + + NodeList nodes = findElementsByPath(xmlDoc, "//*[@ID]"); + + NodeList signatures = xmlDoc.getElementsByTagNameNS(Constants.SignatureSpecNS, Constants._TAG_SIGNATURE); + //check the message is signed - security measure + if(signatures.getLength() == 0){ + return false; + } + for (int i = 0; i < signatures.getLength(); i++) { + Element signedElement = findNodeById(nodes, getSignatureID((Element) signatures.item(i))); + if (signedElement == null) { + return false; + } + signedElement.setIdAttribute("ID", true); + try { + XMLSignature signature = new XMLSignature((Element) signatures.item(i), ""); + //verifies the signature algorithm is one expected - security meassure + if (!verifySignatureAlgorithm((Element) signatures.item(i))) { + return false; + } + if (!signature.checkSignatureValue(cert)) { + return false; + } + } catch (Exception e) { + logger.error("validateSignatures", e); + return false; + } + } + return true; + } + + private static boolean verifySignatureAlgorithm(Element elem) { + logger.trace("verifySignatureAlgorithm"); + NodeList signatureMethod = elem.getElementsByTagNameNS(Constants.SignatureSpecNS, Constants._TAG_SIGNATUREMETHOD); + String signatureAlgorithm = signatureMethod.item(0).getAttributes().getNamedItem(Constants._ATT_ALGORITHM).getNodeValue(); + logger.debug(MessageFormat.format("verifySignatureAlgorithm - algorithm: {0}", signatureAlgorithm)); + String[] algorithm = signatureAlgorithm.split("#"); + List validAlgorithms = Arrays.asList("rsa-sha1", "rsa-sha256", "rsa-sha512"); + for (String alg : validAlgorithms) { + if (algorithm[1].trim().equals(alg)) { + return true; + } + } + logger.error(MessageFormat.format("verifySignatureAlgorithm - Invalid Signature algorithm {0}", algorithm[1])); + return false; + } + + private static String getSignatureID(Element signatureElement) { + return signatureElement.getElementsByTagNameNS(Constants.SignatureSpecNS, Constants._TAG_REFERENCE).item(0).getAttributes().getNamedItem(Constants._ATT_URI).getNodeValue(); + } + + private static NodeList findElementsByPath(Document doc, String xPath) { + logger.trace("findElementsByPath"); + try { + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPath xpath = xpathFactory.newXPath(); + XPathExpression expr = xpath.compile(xPath); + return (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + } catch (Exception e) { + logger.error("findElementsByPath", e); + return null; + } + } + + private static Element findNodeById(NodeList nodes, String id) { + logger.trace("findNodeById"); + if (nodes == null) { + logger.error("findNodeById - Document node list is empty"); + return null; + } + for (int i = 0; i < nodes.getLength(); i++) { + if (nodes.item(i).getAttributes().getNamedItem("ID").getNodeValue().equals(id.substring(1))) { + return (Element) nodes.item(i); + } + } + logger.error(MessageFormat.format("Element with id {0} not found", id.substring(1))); + return null; + } + +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/Encoding.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/Encoding.java new file mode 100644 index 000000000..4b9bfa523 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/Encoding.java @@ -0,0 +1,88 @@ +package com.genexus.saml20.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.util.encoders.Base64; +import org.w3c.dom.Document; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayOutputStream; +import java.io.StringWriter; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; + +public class Encoding { + + private static final Logger logger = LogManager.getLogger(Encoding.class); + + public static String delfateAndEncodeXmlParameter(String parm) { + logger.trace("delfateAndEncodeXmlParameter"); + + try { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater); + deflaterStream.write(parm.getBytes(StandardCharsets.UTF_8)); + deflaterStream.finish(); + + String base64 = Base64.toBase64String(bytesOut.toByteArray()); + logger.debug(MessageFormat.format("Base64: {0}", base64)); + return URLEncoder.encode(base64, StandardCharsets.UTF_8.name()); + } catch (Exception e) { + logger.error("delfateAndEncodeXmlParameter", e); + return ""; + } + } + + public static String decodeAndInflateXmlParameter(String parm) { + logger.trace("decodeAndInflateXmlParameter"); + try { + String base64 = URLDecoder.decode(parm, StandardCharsets.UTF_8.name()); + byte[] bytes = Base64.decode(base64); + byte[] uncompressedData = new byte[4096]; + Inflater inflater = new Inflater(true); + inflater.setInput(bytes); + int len = inflater.inflate(uncompressedData); + inflater.end(); + return new String(uncompressedData, 0, len, StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("decodeAndInflateXmlParameter", e); + return ""; + } + } + + public static String documentToString(Document doc) { + logger.trace("documentToString"); + try (StringWriter writer = new StringWriter()) { + DOMSource domSource = new DOMSource(doc); + StreamResult result = new StreamResult(writer); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(domSource, result); + return writer.toString(); + } catch (Exception e) { + logger.error("documentToString", e); + return null; + } + } + + public static byte[] decodeParameter(String parm) { + logger.trace("decodeParameter"); + try { + String base64 = URLDecoder.decode(parm, StandardCharsets.UTF_8.name()); + return Base64.decode(base64); + } catch (Exception e) { + logger.error("decodeParameter", e); + return null; + } + + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/Hash.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/Hash.java new file mode 100644 index 000000000..7b18ba104 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/Hash.java @@ -0,0 +1,90 @@ +package com.genexus.saml20.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; + +import java.text.MessageFormat; + +public enum Hash { + + SHA1, SHA256, SHA512; + + private static final Logger logger = LogManager.getLogger(Hash.class); + + public static Hash getHash(String hash) { + logger.trace("GetHash"); + switch (hash.toUpperCase().trim()) { + case "SHA1": + return SHA1; + case "SHA256": + return SHA256; + case "SHA512": + return SHA512; + default: + logger.error(MessageFormat.format("GetHash - not implemented signature hash: {0}", hash)); + return null; + } + } + + public static String valueOf(Hash hash) { + switch (hash) { + case SHA1: + return "SHA1"; + case SHA256: + return "SHA256"; + case SHA512: + return "SHA512"; + default: + return ""; + } + } + + public static String getSigAlg(Hash hash) { + logger.trace("GetSigAlg"); + switch (hash) { + case SHA1: + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha1"; + case SHA256: + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + case SHA512: + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; + default: + logger.error("GetSigAlg - not implemented signature hash"); + return ""; + } + } + + public static Digest getDigest(Hash hash) { + logger.trace("getDigest"); + switch (hash) { + case SHA1: + return new SHA1Digest(); + case SHA256: + return new SHA256Digest(); + case SHA512: + return new SHA512Digest(); + default: + logger.error("getDigest - unknown hash"); + return null; + } + } + + public static Hash getHashFromSigAlg(String sigAlg) { + logger.trace("getHashFromSigAlg"); + switch (sigAlg.trim()) { + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha1": + return SHA1; + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": + return SHA256; + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": + return SHA512; + default: + logger.error(MessageFormat.format("getHashFromSigAlg - not implemented signature algorithm: {0}", sigAlg)); + return null; + } + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/Keys.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/Keys.java new file mode 100644 index 000000000..9547bae3f --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/Keys.java @@ -0,0 +1,145 @@ +package com.genexus.saml20.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory; +import org.bouncycastle.util.encoders.Base64; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; + +public class Keys { + + private static final Logger logger = LogManager.getLogger(Keys.class); + + public static AsymmetricKeyParameter loadPrivateKey(String path, String alias, String password) { + return isBase64(path) ? privateKeyFromBase64(path) : loadPrivateKeyFromJKS(path, alias, password); + } + + public static X509Certificate loadCertificate(String path, String alias, String password) { + return isBase64(path) ? loadCertificateFromBase64(path) : loadCertificateFromJKS(path, alias, password); + } + + public static AsymmetricKeyParameter getAsymmetricKeyParameter(X509Certificate cert) { + logger.trace("getAsymmetricKeyParameter"); + try { + PublicKey publicKey = cert.getPublicKey(); + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + return PublicKeyFactory.createKey(subjectPublicKeyInfo); + } catch (Exception e) { + logger.error("getAsymmetricKeyParameter", e); + return null; + } + } + + private static String getCertPath(String path) { + //boolean isAbsolute = new File(path).isAbsolute(); + + logger.debug("cuurent dir: " + new File(".").toPath().toAbsolutePath()); + + return System.getProperty("user.dir"); + } + + private static AsymmetricKeyParameter privateKeyFromBase64(String b64) { + logger.trace("privateKeyFromBase64"); + try { + byte[] keyBytes = Base64.decode(b64); + try (ASN1InputStream istream = new ASN1InputStream(keyBytes)) { + ASN1Sequence seq = (ASN1Sequence) istream.readObject(); + return PrivateKeyFactory.createKey(PrivateKeyInfo.getInstance(seq)); + } + } catch (Exception e) { + logger.error("privateKeyFromBase64", e); + return null; + } + } + + private static X509Certificate loadCertificateFromBase64(String b64) { + logger.trace("loadCertificateFromBase64"); + try { + byte[] dataBuffer = Base64.decode(b64); + ByteArrayInputStream bI = new ByteArrayInputStream(dataBuffer); + CertificateFactory cf = new CertificateFactory(); + return (X509Certificate) cf.engineGenerateCertificate(bI); + } catch (Exception e) { + logger.error("loadCertificateFromBase64", e); + return null; + } + } + + private static AsymmetricKeyParameter loadPrivateKeyFromJKS(String path, String alias, String password) { + logger.trace("loadPrivateKeyFromJKS"); + logger.debug(MessageFormat.format("Path: {0} , alias: {1}", path, alias)); + getCertPath(path); + try (InputStream in = new DataInputStream(Files.newInputStream(new File(path).toPath()))) { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(in, password.toCharArray()); + if (alias.isEmpty()) { + alias = ks.aliases().nextElement(); + } + if (ks.getKey(alias, password.toCharArray()) != null) { + PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + PrivateKeyInfo keyinfo = PrivateKeyInfo.getInstance(privateKey.getEncoded()); + return PrivateKeyFactory.createKey(keyinfo); + } + } catch (Exception e) { + logger.error("loadPrivateKeyFromJKS", e); + return null; + } + return null; + } + + private static X509Certificate loadCertificateFromJKS(String path, String alias, String password) { + logger.trace("loadCertificateFromJKS"); + logger.debug("alias: " + alias); + logger.debug("pasword: " + password); + logger.debug(MessageFormat.format("path: {0}, alias: {1}", path, alias)); + Path p = new File(path).toPath(); + logger.debug("Res path: " + p.toAbsolutePath()); + System.out.println("Res path: " + p.toAbsolutePath()); + try (InputStream in = new DataInputStream(Files.newInputStream(p))) { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(in, password.toCharArray()); + if (alias.isEmpty()) { + alias = ks.aliases().nextElement(); + } + return (X509Certificate) ks.getCertificate(alias); + + } catch (Exception e) { + logger.error("loadCertificateFromJKS", e); + return null; + } + + } + + public static boolean isBase64(String path) { + try { + Base64.decode(path); + return true; + } catch (Exception e) { + return false; + } + } + + public static String getHash(String path, String alias, String password) { + String algorithmWithHash = loadCertificateFromJKS(path, alias, password).getSigAlgName(); + String[] aux = algorithmWithHash.toUpperCase().split("WITH"); + return aux[0]; + } +} \ No newline at end of file diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/SamlAssertionUtils.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/SamlAssertionUtils.java new file mode 100644 index 000000000..b8b91ee5c --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/SamlAssertionUtils.java @@ -0,0 +1,301 @@ +package com.genexus.saml20.utils; + +import com.genexus.saml20.utils.xml.Attribute; +import com.genexus.saml20.utils.xml.Element; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.xml.security.c14n.Canonicalizer; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public class SamlAssertionUtils { + + private static final Logger logger = LogManager.getLogger(SamlAssertionUtils.class); + + private static final String _saml_protocolNS = "urn:oasis:names:tc:SAML:2.0:protocol"; //saml2p + private static final String _saml_assertionNS = "urn:oasis:names:tc:SAML:2.0:assertion"; //saml2 + + public static Document loadDocument(String xml) { + logger.trace("loadDocument"); + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + + //disable parser's DTD reading - security meassure + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + + DocumentBuilder db = dbf.newDocumentBuilder(); + return db.parse(inputStream); + } catch (Exception e) { + logger.error("loadDocument", e); + return null; + } + } + + public static boolean isLogout(Document xmlDoc){ + logger.trace("isLogout"); + try { + return xmlDoc.getDocumentElement().getLocalName().equals("LogoutResponse"); + }catch (Exception e) + { + logger.error("isLogout", e); + return false; + } + } + + public static Document createLogoutRequest(String id, String issuer, String nameID, String sessionIndex, String destination) { + logger.trace("createLogoutRequest"); + + ZonedDateTime nowUtc = ZonedDateTime.now(java.time.ZoneOffset.UTC); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + String issueInstant = nowUtc.format(formatter); + + + String saml2p = "urn:oasis:names:tc:SAML:2.0:protocol"; + String saml2 = "urn:oasis:names:tc:SAML:2.0:assertion"; + + DocumentBuilder builder = createDocumentBuilder(); + + assert builder != null; + Document doc = builder.newDocument(); + + org.w3c.dom.Element request = doc.createElementNS(saml2p, "saml2p:LogoutRequest"); + request.setAttribute("ID", id); + request.setAttribute("Version", "2.0"); + request.setAttribute("IssueInstant", issueInstant); + //request.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + request.setAttribute("Destination", destination); + request.setAttribute("Reason", "urn:oasis:names:tc:SAML:2.0:logout:user"); + + org.w3c.dom.Element issuerElem = doc.createElementNS(saml2, "saml2:Issuer"); + issuerElem.setTextContent(issuer); + request.appendChild(issuerElem); + + org.w3c.dom.Element nameIDElem = doc.createElementNS(saml2, "saml2:NameID"); + nameIDElem.setTextContent(nameID); + request.appendChild(nameIDElem); + + org.w3c.dom.Element sessionElem = doc.createElementNS(saml2p, "saml2p:SessionIndex"); + sessionElem.setTextContent(sessionIndex); + request.appendChild(sessionElem); + + doc.appendChild(request); + + logger.debug(MessageFormat.format("createLogoutRequest - XML request: {0}", Encoding.documentToString(doc))); + return doc; + } + + public static Document createLoginRequest(String id, String destination, String acsUrl, String issuer, String policyFormat, String authContext, String spname, boolean forceAuthn) { + logger.trace("createLoginRequest"); + + ZonedDateTime nowUtc = ZonedDateTime.now(java.time.ZoneOffset.UTC); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + String issueInstant = nowUtc.format(formatter); + + String samlp = "urn:oasis:names:tc:SAML:2.0:protocol"; + String saml = "urn:oasis:names:tc:SAML:2.0:assertion"; + + + DocumentBuilder builder = createDocumentBuilder(); + + assert builder != null; + Document doc = builder.newDocument(); + + org.w3c.dom.Element request = doc.createElementNS(samlp, "saml2p:AuthnRequest"); + request.setAttribute("ID", id); + request.setAttribute("Version", "2.0"); + request.setAttribute("IssueInstant", issueInstant); + //request.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + request.setAttribute("Destination", destination); + request.setAttribute("AssertionConsumerServiceURL", acsUrl); + request.setAttribute("ForceAuthn", Boolean.toString(forceAuthn)); + + org.w3c.dom.Element issuerElem = doc.createElementNS(saml, "saml2:Issuer"); + issuerElem.setTextContent(issuer); + request.appendChild(issuerElem); + + org.w3c.dom.Element policy = doc.createElementNS(samlp, "saml2p:NameIDPolicy"); + policy.setAttribute("Format", policyFormat.trim()); + policy.setAttribute("AllowCreate", "true"); + policy.setAttribute("SPNameQualifier", spname); + request.appendChild(policy); + + org.w3c.dom.Element authContextElem = doc.createElementNS(samlp, "saml2p:RequestedAuthnContext"); + authContextElem.setAttribute("Comparison", "exact"); + + org.w3c.dom.Element authnContextClass = doc.createElementNS(saml, "saml2:AuthnContextClassRef"); + authnContextClass.setTextContent(authContext); + authContextElem.appendChild(authnContextClass); + request.appendChild(authContextElem); + + + doc.appendChild(request); + + logger.debug(MessageFormat.format("CreateLoginRequest - XML request: {0}", Encoding.documentToString(doc))); + return doc; + } + + private static DocumentBuilder createDocumentBuilder() { + logger.trace("createDocumentBuilder"); + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + return factory.newDocumentBuilder(); + } catch (Exception e) { + logger.error("createDocumentBuilder", e); + return null; + } + } + + public static Document canonicalizeXml(String xml) { + //delete comments from the xml - security meassure + logger.trace("canoncalizeXml"); + logger.debug(MessageFormat.format("xmlString: {0}", xml)); + try { + org.apache.xml.security.Init.init(); + + Document doc = loadDocument(xml); + + Canonicalizer canonicalizer = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + canonicalizer.canonicalizeSubtree(doc, out); + + String canonicalizedXML = out.toString(StandardCharsets.UTF_8.name()); + + return loadDocument(canonicalizedXML); + + } catch (Exception e) { + logger.error("canoncalizeXml", e); + return null; + } + } + + public static String getLoginInfo(Document xmlDoc) { + List atributeList = new ArrayList(); + atributeList.add(new Attribute(_saml_assertionNS, "SubjectConfirmationData", "InResponseTo")); + atributeList.add(new Attribute(_saml_assertionNS, "Conditions", "NotOnOrAfter")); + atributeList.add(new Attribute(_saml_assertionNS, "Conditions", "NotBefore")); + atributeList.add(new Attribute(_saml_assertionNS, "SubjectConfirmationData", "Recipient")); + atributeList.add(new Attribute(_saml_assertionNS, "AuthnStatement", "SessionIndex")); + atributeList.add(new Attribute(_saml_protocolNS, "Response", "Destination")); + atributeList.add(new Attribute(_saml_protocolNS, "StatusCode", "Value")); + + List elementList = new ArrayList(); + elementList.add(new Element(_saml_assertionNS, "Issuer")); + elementList.add(new Element(_saml_assertionNS, "Audience")); + elementList.add(new Element(_saml_assertionNS, "NameID")); + + return printJson(xmlDoc, atributeList, elementList); + } + + public static String getLogoutInfo(Document doc) { + logger.trace("getLogoutInfo"); + List atributeList = new ArrayList(); + atributeList.add(new Attribute(_saml_protocolNS, "LogoutResponse", "Destination")); + atributeList.add(new Attribute(_saml_protocolNS, "LogoutResponse", "InResponseTo")); + atributeList.add(new Attribute(_saml_protocolNS, "StatusCode", "Value")); + + List elementList = new ArrayList(); + elementList.add(new Element(_saml_assertionNS, "Issuer")); + + return printJson(doc, atributeList, elementList); + } + + public static String getLoginAttribute(Document doc, String name) { + logger.trace("getLoginAttribute"); + NodeList nodes = getAtttributeElements(doc); + + for (int i = 0; i < nodes.getLength(); i++) { + if (nodes.item(i).getAttributes().getNamedItem("Name").getNodeValue().equals(name)) { + String value = nodes.item(i).getTextContent() == null ? getAttributeContent(nodes.item(i)): nodes.item(i).getTextContent(); + logger.debug(MessageFormat.format("getLoginAttribute -- attribute name: {0}, value: {1}", name, value)); + return value; + } + } + logger.error(MessageFormat.format("getLoginAttribute -- Could not find attribute with name {0}", name)); + return ""; + } + + public static String getRoles(Document doc, String name) { + logger.trace("getRoles"); + NodeList nodes = getAtttributeElements(doc); + List roles = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + if (nodes.item(i).getAttributes().getNamedItem("Name").getNodeValue().equals(name)) { + NodeList nList = nodes.item(i).getChildNodes(); + for (int j = 0; j < nList.getLength(); j++) { + if (!nList.item(j).getTextContent().trim().isEmpty()) { + roles.add(nList.item(j).getTextContent().trim()); + } + } + if (roles.isEmpty()) { + NodeList eList = ((org.w3c.dom.Element) nodes.item(i)).getElementsByTagName("AttributeValue"); + for (int j = 0; j < eList.getLength(); j++) { + if (!nList.item(j).getTextContent().trim().isEmpty()) { + roles.add(nList.item(j).getTextContent().trim()); + } + } + } + return String.join(",", roles); + + } + } + logger.debug(MessageFormat.format("GetRoles -- Could not find attribute with name {0}", name)); + return ""; + } + + private static String getAttributeContent(Node node) { + String value = node.getChildNodes().item(0).getTextContent().trim(); + return value.isEmpty() ? ((org.w3c.dom.Element) node).getElementsByTagName("AttributeValue").item(0).getTextContent() : value; + } + + private static NodeList getAtttributeElements(Document doc) { + NodeList nodes = doc.getElementsByTagNameNS(_saml_assertionNS, "Attribute"); + return nodes.getLength() == 0 ? doc.getElementsByTagName("Attribute") : nodes; + } + + private static String printJson(Document xmlDoc, List atributes, List elements) { + logger.trace("PrintJson"); + StringBuilder json = new StringBuilder("{"); + for (Attribute at : atributes) { + String value = at.printJson(xmlDoc); + if (value != null) { + json.append(MessageFormat.format("{0},", value)); + } + + } + + int counter = 0; + for (Element el : elements) { + String value = el.printJson(xmlDoc); + if (value != null) { + if (counter != elements.size() - 1) { + json.append(MessageFormat.format("{0},", value)); + } else { + json.append(MessageFormat.format("{0} }", value)); + } + } + counter++; + } + logger.debug(MessageFormat.format("printJson -- json: {0}", json.toString())); + return json.toString(); + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Attribute.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Attribute.java new file mode 100644 index 000000000..132898ed1 --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Attribute.java @@ -0,0 +1,29 @@ +package com.genexus.saml20.utils.xml; + +import org.w3c.dom.Document; + +import java.text.MessageFormat; + +public class Attribute extends XmlTypes { + + Element element; + private final String tag; + + public Attribute(String namespace, String element, String tag) { + this.element = new Element(namespace, element); + this.tag = tag; + } + + public String getTag() { + return tag; + } + + public String findValue(Document doc) { + return element.getElement(doc).getAttributes().getNamedItem(tag).getNodeValue(); + } + + public String printJson(Document xmlDoc) { + String value = findValue(xmlDoc); + return value == null ? null : MessageFormat.format("\"{0}\": \"{1}\"", tag, value); + } +} \ No newline at end of file diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Element.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Element.java new file mode 100644 index 000000000..4337910ab --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/Element.java @@ -0,0 +1,37 @@ +package com.genexus.saml20.utils.xml; + +import org.w3c.dom.Document; + +import java.text.MessageFormat; + +public class Element extends XmlTypes { + + private final String namespace; + private final String tag; + + public Element(String namespace, String tag) { + this.namespace = namespace; + this.tag = tag; + } + + public String getNamespace() { + return namespace; + } + + public String getTag() { + return tag; + } + + public org.w3c.dom.Node getElement(Document doc) { + return doc.getElementsByTagNameNS(namespace, tag).item(0); + } + + public String findValue(Document doc) { + return getElement(doc).getTextContent(); + } + + public String printJson(Document xmlDoc) { + String value = findValue(xmlDoc); + return value == null ? null : MessageFormat.format("\"{0}\": \"{1}\"", tag, value); + } +} diff --git a/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/XmlTypes.java b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/XmlTypes.java new file mode 100644 index 000000000..03eb2170e --- /dev/null +++ b/gamsaml20/src/main/java/com/genexus/saml20/utils/xml/XmlTypes.java @@ -0,0 +1,8 @@ +package com.genexus.saml20.utils.xml; + +import org.w3c.dom.Document; + +public abstract class XmlTypes { + + abstract String printJson(Document doc); +} diff --git a/gamsaml20/src/test/java/com/genexus/test/PostBindingTest.java b/gamsaml20/src/test/java/com/genexus/test/PostBindingTest.java new file mode 100644 index 000000000..b350d1f8e --- /dev/null +++ b/gamsaml20/src/test/java/com/genexus/test/PostBindingTest.java @@ -0,0 +1,84 @@ +package com.genexus.test; + +import com.genexus.saml20.PostBinding; +import com.genexus.saml20.SamlParms; +import org.bouncycastle.util.encoders.Base64; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class PostBindingTest { + + public static final String resources = System.getProperty("user.dir").concat("/src/test/resources"); + + private static PostBinding postBindingLoginResponse; + private static PostBinding postBindingLogoutResponse; + private static String alias; + private static String password; + + @BeforeClass + public static void setUp() { + postBindingLoginResponse = new PostBinding(); + String b64Login = "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfOGU4ZGM1ZjY5YTk4Y2M0YzFmZjM0MjdlNWNlMzQ2MDZmZDY3MmY5MWU2IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl80ZmVlM2IwNDYzOTVjNGU3NTEwMTFlOTdmODkwMGI1MjczZDU2Njg1Ij4KICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPgogIDxzYW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIiAvPgogIDwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJfZDcxYTNhOGU5ZmNjNDVjOWU5ZDI0OGVmNzA0OTM5M2ZjOGYwNGU1Zjc1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiPgogICAgPHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4KICAgIDxzYW1sOlN1YmplY3Q+CiAgICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9zcC5leGFtcGxlLmNvbS9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5fY2UzZDI5NDhiNGNmMjAxNDZkZWUwYTBiM2RkNmY2OWI2Y2Y4NmY2MmQ3PC9zYW1sOk5hbWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyNC0wMS0xOFQwNjoyMTo0OFoiIFJlY2lwaWVudD0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNGZlZTNiMDQ2Mzk1YzRlNzUxMDExZTk3Zjg5MDBiNTI3M2Q1NjY4NSIgLz4KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTA3LTE3VDAxOjAxOjE4WiIgTm90T25PckFmdGVyPSIyMDI0LTAxLTE4VDA2OjIxOjQ4WiI+CiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L3NhbWw6Q29uZGl0aW9ucz4KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjQtMDctMTdUMDk6MDE6NDhaIiBTZXNzaW9uSW5kZXg9Il9iZTk5NjdhYmQ5MDRkZGNhZTNjMGViNDE4OWFkYmUzZjcxZTMyN2NmOTMiPgogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXJzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmV4YW1wbGVyb2xlMTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgPFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy14bWwtYzE0bi0yMDAxMDMxNSIgLz48U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIgLz48UmVmZXJlbmNlIFVSST0iI19kNzFhM2E4ZTlmY2M0NWM5ZTlkMjQ4ZWY3MDQ5MzkzZmM4ZjA0ZTVmNzUiPjxUcmFuc2Zvcm1zPjxUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIgLz48L1RyYW5zZm9ybXM+PERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIgLz48RGlnZXN0VmFsdWU+dWNwMm1qT2ZCNDZaaWxHbFpKUzhWR0VMVWpRZ0xQdnl3d3hIVjJHdjNmdz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU+cUdSWExNVHF4dHRqWldrWkovOEo5MWZkTVNSa2NZbW9DUTVhaFhGYUk5QzNobmlyQ0lUVjNIM1M1VDRiQTg0RC9tRVcyZDkwNmZ6ZnVhZWR1TnJRMkU1U0VtTUFFV25EdEwyak9GTzBrSGlZMHdoS2s4TkZSRnc2eC9kVk5uVUxQOXhnOGN5cmV6dVNxY1VSd1RWKzlhMXI4NGQ2VlpoLzJLTTg3WUMrODh2RVN2QXBUTXdmMGI2RUdmb3FvbDlJNG9KMDcrME9KMEdyU3VFajExTU1hTkZNakhTYmVKOXpGaWluV0lPSUVRdW9XRWVrUHhLNzQxTzJyMTNyNm5PbnVSajIwdThuVmRLb2tncGR2L1VwL0VtOVNGUVFsVDN5TUFhY0JlbjhPL1ZLOTF3TjdMbTQvZkpkWjF1UTFHOHVrcTRLSHZZWGc3K3VyKzJHbHVYY2JRPT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSUVBVENDQXVtZ0F3SUJBZ0lKQUlBcXZLSForZ0ZoTUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdXTVFzd0NRWURWUVFHRXdKVldURVRNQkVHQTFVRUNBd0tUVzl1ZEdWMmFXUmxiekVUTUJFR0ExVUVCd3dLVFc5dWRHVjJhV1JsYnpFUU1BNEdBMVVFQ2d3SFIyVnVaVmgxY3pFUk1BOEdBMVVFQ3d3SVUyVmpkWEpwZEhreEVqQVFCZ05WQkFNTUNYTm5jbUZ0Y0c5dVpURWtNQ0lHQ1NxR1NJYjNEUUVKQVJZVmMyZHlZVzF3YjI1bFFHZGxibVY0ZFhNdVkyOXRNQjRYRFRJd01EY3dPREU0TlRjeE4xb1hEVEkxTURjd056RTROVGN4TjFvd2daWXhDekFKQmdOVkJBWVRBbFZaTVJNd0VRWURWUVFJREFwTmIyNTBaWFpwWkdWdk1STXdFUVlEVlFRSERBcE5iMjUwWlhacFpHVnZNUkF3RGdZRFZRUUtEQWRIWlc1bFdIVnpNUkV3RHdZRFZRUUxEQWhUWldOMWNtbDBlVEVTTUJBR0ExVUVBd3dKYzJkeVlXMXdiMjVsTVNRd0lnWUpLb1pJaHZjTkFRa0JGaFZ6WjNKaGJYQnZibVZBWjJWdVpYaDFjeTVqYjIwd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUMxemdhVStXaDYzcDlETldvQXk2NDI1MkV2WmpONDlBWTN4MFFDbkFhOEpPOVBrN3puUXdyeEVGVUtnWnp2MEdIRVlXNytYK3V5SnI3Qlc0VEE2ZnVKSjhhZ0UvYm1aUlp5amRKam91ZTBGTUw2ZmJtQ1o5VHN4cHhlNHB6aXNweVdROGpZVDRLbDRJM2ZkWk5VU240WFNpZG5ES0JJU2VDMDVtcmNjaERLaElucGlZREo0ODFsc0I0SlRFdGkzUzRYeS9Ub0t3WTR0NmF0dHc2ejVRRGhCYytZcm8rWVVxcnVsaU9BS3FjZnliZTlrMDdqd01DdkZWTTFocllZSjdod0hEU0ZvM01Ld1oweTJndzB3NlNnVkJ4TEZvK0tZUDNxNjNiNXdWaEQ4bHphU2grOFVjeWlITTIveWpFZWo3RW5SRnpkY2xUU05YUkZOYWlMbkVWZEFnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCUXRRQVdKUldOci9Pc3dQU0Fkd0NRaDBFZWkvREFmQmdOVkhTTUVHREFXZ0JRdFFBV0pSV05yL09zd1BTQWR3Q1FoMEVlaS9EQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDakhlM0piTkt2MFl3YzF6bExhY1VOV2NqTGJtenZuanM4V3E1b3h0ZjV3RzVQVWxoTFNZWjlNUGh1Zjk1UGxpYm5yTy94VlkyOTJQNWxvNE5LaFM3Vk9vbnBiUFEvUHJDTU84NFB6MUxHZk0vd0NXUUlvd2g2VkhxMThQaVprYTl6YndsNlNvMHRnQ2xLa0ZTUms0d3BLcldYMytNMytZK0QwYnJkOHNFdEE2ZFhlWUhEdHFWMFlnaktkWklJT3gwdkRUNGFsQ29WUXJRMXlBSXE1SU5UM2NTTGdKZXpJaEVhZER2M1RjN2JNeE1GZUwrODFxSG05Wi85L0tFNlorSkIwWkVPa0YvMk5TUUpkK1o3TUJSOEN4T2RUUWlzM2x0TW9YRGF0TmtqWjJFbnY0MHN3NE5JQ0I4WVloc1dNSWFyZXc1dU5UK1JTMjhZSE5sYm1vZ2g8L1g1MDlDZXJ0aWZpY2F0ZT48L1g1MDlEYXRhPjwvS2V5SW5mbz48L1NpZ25hdHVyZT48L3NhbWw6QXNzZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPg=="; + postBindingLoginResponse.init(new String(Base64.decode(b64Login), StandardCharsets.UTF_8)); + + postBindingLogoutResponse = new PostBinding(); + String b64Logout = "PHNhbWxwOkxvZ291dFJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNmMzNzM3MjgyZjAwNzcyMGU3MzZmMGY0MDI4ZmVlZDhjYjliNDAyOTFjIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wNy0xOFQwMToxMzowNloiIERlc3RpbmF0aW9uPSJodHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl8yMWRmOTFhODk3Njc4NzlmYzBmN2RmNmExNDkwYzYwMDBjODE2NDRkIj4NCiAgPHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIiAvPg0KICA8L3NhbWxwOlN0YXR1cz4NCjxTaWduYXR1cmUgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxTaWduZWRJbmZvPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMteG1sLWMxNG4tMjAwMTAzMTUiIC8+PFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiIC8+PFJlZmVyZW5jZSBVUkk9IiNfNmMzNzM3MjgyZjAwNzcyMGU3MzZmMGY0MDI4ZmVlZDhjYjliNDAyOTFjIj48VHJhbnNmb3Jtcz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiIC8+PC9UcmFuc2Zvcm1zPjxEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiIC8+PERpZ2VzdFZhbHVlPlYwdDFxcVQzaENuNVJsYmFqWU5Va21wYUxpRzhsVXRobWx4aW1vMzJ5dEk9PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8+PFNpZ25hdHVyZVZhbHVlPkJId2ZzS3lzSXZFMlh3ajdiVDVlRzBIVER5NXFrK25jUjRGQmlsZDI3d3dnWG9UTDdnMXZ1bDVsNCtIeU9QeUZMMXdmbC9DTWR4SE9pNDBLcytOTXJWcUpKRkRDT1NqdGJDWGdHQTg2ZzV1KzRjM2ZRNlNSckxMbUJZa0JSdjlGeGRCVzdTbEhGM2hiall2azFLVXNRV1llTVR5bW00UERHSCtuNGtsbmdFcklrWDBvSHZHcUJDZmt0bGdWTmwvanpHcWNoUnk1Tnpta2lrZGd5bGNJUUZjNS9hUWlEYXE0ODMrZVBhVjIrVHdKNWlBeFRrRXFsVHBLMGVZcjNILzkwRXpid1BaL0F0TjB1RGk4YkdZYmVBeUszR3oyVWFKUEpVWHpzM1dMWDc4UTN1a3hIM0FxaUpHM21PZUIvclBDeDdSeVBrZEZNS2xPeHVkTWM5NDBkdz09PC9TaWduYXR1cmVWYWx1ZT48S2V5SW5mbz48WDUwOURhdGE+PFg1MDlDZXJ0aWZpY2F0ZT5NSUlFQVRDQ0F1bWdBd0lCQWdJSkFJQXF2S0haK2dGaE1BMEdDU3FHU0liM0RRRUJDd1VBTUlHV01Rc3dDUVlEVlFRR0V3SlZXVEVUTUJFR0ExVUVDQXdLVFc5dWRHVjJhV1JsYnpFVE1CRUdBMVVFQnd3S1RXOXVkR1YyYVdSbGJ6RVFNQTRHQTFVRUNnd0hSMlZ1WlZoMWN6RVJNQThHQTFVRUN3d0lVMlZqZFhKcGRIa3hFakFRQmdOVkJBTU1DWE5uY21GdGNHOXVaVEVrTUNJR0NTcUdTSWIzRFFFSkFSWVZjMmR5WVcxd2IyNWxRR2RsYm1WNGRYTXVZMjl0TUI0WERUSXdNRGN3T0RFNE5UY3hOMW9YRFRJMU1EY3dOekU0TlRjeE4xb3dnWll4Q3pBSkJnTlZCQVlUQWxWWk1STXdFUVlEVlFRSURBcE5iMjUwWlhacFpHVnZNUk13RVFZRFZRUUhEQXBOYjI1MFpYWnBaR1Z2TVJBd0RnWURWUVFLREFkSFpXNWxXSFZ6TVJFd0R3WURWUVFMREFoVFpXTjFjbWwwZVRFU01CQUdBMVVFQXd3SmMyZHlZVzF3YjI1bE1TUXdJZ1lKS29aSWh2Y05BUWtCRmhWelozSmhiWEJ2Ym1WQVoyVnVaWGgxY3k1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDMXpnYVUrV2g2M3A5RE5Xb0F5NjQyNTJFdlpqTjQ5QVkzeDBRQ25BYThKTzlQazd6blF3cnhFRlVLZ1p6djBHSEVZVzcrWCt1eUpyN0JXNFRBNmZ1Sko4YWdFL2JtWlJaeWpkSmpvdWUwRk1MNmZibUNaOVRzeHB4ZTRwemlzcHlXUThqWVQ0S2w0STNmZFpOVVNuNFhTaWRuREtCSVNlQzA1bXJjY2hES2hJbnBpWURKNDgxbHNCNEpURXRpM1M0WHkvVG9Ld1k0dDZhdHR3Nno1UURoQmMrWXJvK1lVcXJ1bGlPQUtxY2Z5YmU5azA3andNQ3ZGVk0xaHJZWUo3aHdIRFNGbzNNS3daMHkyZ3cwdzZTZ1ZCeExGbytLWVAzcTYzYjV3VmhEOGx6YVNoKzhVY3lpSE0yL3lqRWVqN0VuUkZ6ZGNsVFNOWFJGTmFpTG5FVmRBZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlF0UUFXSlJXTnIvT3N3UFNBZHdDUWgwRWVpL0RBZkJnTlZIU01FR0RBV2dCUXRRQVdKUldOci9Pc3dQU0Fkd0NRaDBFZWkvREFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2pIZTNKYk5LdjBZd2MxemxMYWNVTldjakxibXp2bmpzOFdxNW94dGY1d0c1UFVsaExTWVo5TVBodWY5NVBsaWJuck8veFZZMjkyUDVsbzROS2hTN1ZPb25wYlBRL1ByQ01PODRQejFMR2ZNL3dDV1FJb3doNlZIcTE4UGlaa2E5emJ3bDZTbzB0Z0NsS2tGU1JrNHdwS3JXWDMrTTMrWStEMGJyZDhzRXRBNmRYZVlIRHRxVjBZZ2pLZFpJSU94MHZEVDRhbENvVlFyUTF5QUlxNUlOVDNjU0xnSmV6SWhFYWREdjNUYzdiTXhNRmVMKzgxcUhtOVovOS9LRTZaK0pCMFpFT2tGLzJOU1FKZCtaN01CUjhDeE9kVFFpczNsdE1vWERhdE5raloyRW52NDBzdzROSUNCOFlZaHNXTUlhcmV3NXVOVCtSUzI4WUhObGJtb2doPC9YNTA5Q2VydGlmaWNhdGU+PC9YNTA5RGF0YT48L0tleUluZm8+PC9TaWduYXR1cmU+PC9zYW1scDpMb2dvdXRSZXNwb25zZT4="; + postBindingLogoutResponse.init(new String(Base64.decode(b64Logout), StandardCharsets.UTF_8)); + alias = "1"; + password = "dummy1"; + } + + @Test + public void testSignatureValidation_true() { + SamlParms parms = new SamlParms(); + parms.setTrustCertPath(resources.concat("/keystore.jks")); + parms.setTrustCertAlias(alias); + parms.setTrustCertPass(password); + Assert.assertTrue("testSignatureValidation_true Login", postBindingLoginResponse.verifySignatures(parms)); + Assert.assertTrue("testSignatureValidation_true Logout", postBindingLogoutResponse.verifySignatures(parms)); + } + + @Test + public void testSignatureValidation_false() { + SamlParms parms = new SamlParms(); + parms.setTrustCertPath(resources.concat("/mykeystore.jks")); + parms.setTrustCertAlias(alias); + parms.setTrustCertPass(password); + Assert.assertFalse("testSignatureValidation_false Login", postBindingLoginResponse.verifySignatures(parms)); + Assert.assertFalse("testSignatureValidation_false Logout", postBindingLogoutResponse.verifySignatures(parms)); + } + + @Test + public void testIsLogout() { + Assert.assertFalse("testIsLogout Login", postBindingLoginResponse.isLogout()); + Assert.assertTrue("testIsLogout Logout", postBindingLogoutResponse.isLogout()); + } + + @Test + public void testGetLoginAssertions() { + String expected = "{\"InResponseTo\": \"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685\",\"NotOnOrAfter\": \"2024-01-18T06:21:48Z\",\"NotBefore\": \"2014-07-17T01:01:18Z\",\"Recipient\": \"http://sp.example.com/demo1/index.php?acs\",\"SessionIndex\": \"_be9967abd904ddcae3c0eb4189adbe3f71e327cf93\",\"Destination\": \"http://sp.example.com/demo1/index.php?acs\",\"Value\": \"urn:oasis:names:tc:SAML:2.0:status:Success\",\"Issuer\": \"http://idp.example.com/metadata.php\",\"Audience\": \"http://sp.example.com/demo1/metadata.php\",\"NameID\": \"_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7\" }"; + Assert.assertEquals(expected, postBindingLoginResponse.getLoginAssertions()); + } + + @Test + public void testGetLogoutAssertions() { + String expected = "{\"Destination\": \"http://sp.example.com/demo1/index.php?acs\",\"InResponseTo\": \"ONELOGIN_21df91a89767879fc0f7df6a1490c6000c81644d\",\"Value\": \"urn:oasis:names:tc:SAML:2.0:status:Success\",\"Issuer\": \"http://idp.example.com/metadata.php\" }"; + Assert.assertEquals(expected, postBindingLogoutResponse.getLogoutAssertions()); + } + + @Test + public void testGetRoles() { + String expected = "users,examplerole1"; + Assert.assertEquals(expected, postBindingLoginResponse.getRoles("eduPersonAffiliation")); + } + + @Test + public void testGetLoginAttribute() { + String expected = "test@example.com"; + Assert.assertEquals(expected, postBindingLoginResponse.getLoginAttribute("mail")); + } + +} diff --git a/gamsaml20/src/test/java/com/genexus/test/RedirectBindingTest.java b/gamsaml20/src/test/java/com/genexus/test/RedirectBindingTest.java new file mode 100644 index 000000000..003058c23 --- /dev/null +++ b/gamsaml20/src/test/java/com/genexus/test/RedirectBindingTest.java @@ -0,0 +1,240 @@ +package com.genexus.test; + +import com.genexus.saml20.RedirectBinding; +import com.genexus.saml20.SamlParms; +import com.genexus.saml20.utils.Encoding; +import com.genexus.saml20.utils.Hash; +import com.genexus.saml20.utils.Keys; +import com.genexus.saml20.utils.SamlAssertionUtils; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.signers.RSADigestSigner; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3c.dom.Document; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +public class RedirectBindingTest { + + public static final String resources = System.getProperty("user.dir").concat("/src/test/resources"); + + private static String alias; + private static String password; + private static RedirectBinding redirectBindingLogoutResponse; + + @BeforeClass + public static void setUp() { + alias = "1"; + password = "dummy1"; + + redirectBindingLogoutResponse = new RedirectBinding(); + String logoutResponse = "SAMLResponse=fVHLauQwEPwVo7tsSX7JwnYICVkCyR52ZnPYyyBLPYnBozZueZPPX8%2BEOQSWHJvqquqqbm8%2BTlPyFxYaMXRMpoIlEBz6Mbx27Pf%2BgWuWULTB2wkDdCwgu%2BlbsqdpNk%2F4imv8BTRjIEg2pUDmAnVsXYJBSyOZYE9AJjqzu31%2BMioVZl4wosOJJfdAcQw2XszfYpzJZNmEzk5vSDH7cft8OLMOdxgCuIjLT4gPmaUJU0vzB0se7zt2GKq8sMo7nov6yItKAdd60FwcRV0rXRWFhm01XC%2Fd40aq68ZrbS23FQheQJVz7SvNS%2B%2B9ODYbLO1GIlrhMZwbiB1TQpVclFw2e1kaKY3KU1XWf1jycm1wy8f69kJbPhv5vgtLBMs5P%2Buv%2BSlS%2Bj4Gj%2B%2BUBohZCa4ehmPDpa7VFs81fFBCcitBatk01ZCXWZt9el5%2Fs4s2rvR1ukMPyYudVvj%2BJrpsm93qHBCxrG%2Bzr6LZ%2F%2F7f%2FwM%3D&RelayState=http%3A%2F%2Frelaystate.com&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=ZhxqgSDAmtwxtUAXCafCNAXKLwL9iPgsqInuZfQ97dyPsGyszpgJftjgHBtoQpz159NjFpX0dGicVier2TQa82JBqgxUvdPT6mg%2FppdG7Z%2BnOXNttflqCd7mA3b%2FUOmWE4XgODz2mym%2BNPBmETAYmKofXo5ghpQc8IgGpI166%2F5VOwwhLcrg76HeYSxubxS4BoFUtLmpRnkaww9VQPZPIyh4kBmsCqe%2FV4QvM626ehdXDjPIciBgylt2ENMfQGZo83ubMB7KxgDNdErBgmTpILxftLn3ZH0FJAbM%2B3bzj6DFJ1yLuyUnUbdxOjoKaRskil853jKqmbvQtxRQ4QvZIg%3D%3D"; + redirectBindingLogoutResponse.init(logoutResponse); + } + + @Test + public void testSignatureValidation_true() { + SamlParms parms = new SamlParms(); + parms.setTrustCertPath(resources.concat("/mykeystore.jks")); + parms.setTrustCertAlias(alias); + parms.setTrustCertPass(password); + Assert.assertTrue("testSignatureValidation_true Logout", redirectBindingLogoutResponse.verifySignatures(parms)); + } + + @Test + public void testSignatureValidation_false() { + SamlParms parms = new SamlParms(); + parms.setTrustCertPath(resources.concat("/keystore.jks")); + parms.setTrustCertAlias(alias); + parms.setTrustCertPass(password); + Assert.assertFalse("testSignatureValidation_false Logout", redirectBindingLogoutResponse.verifySignatures(parms)); + } + + + @Test + public void testGetLogoutAssertions() { + String expected = "{\"Destination\": \"https://localhost/GAM_SAML_ConnectorNetF/aslo.aspx\",\"InResponseTo\": \"_779d88aa-a6e0-4e63-8d68-5ddd0f97791a\",\"Value\": \"urn:oasis:names:tc:SAML:2.0:status:Success\",\"Issuer\": \"https://sts.windows.net/5ec7bbf9-1872-46c9-b201-a1e181996b35/\" }"; + Assert.assertEquals(expected, redirectBindingLogoutResponse.getLogoutAssertions()); + } + + @Test + public void testIsLogout() { + String loginResponse = "SAMLResponse=5Vbfb%2BJGEH7uSf0frH03%2FoGNjRW40qSpkJJcFOipvZdoWQ%2FgO3vX3V0Hcn99Zw0mYHqE3lWqTn2yPDOe%2Beb7dmZ98XZd5NYTSJUJPiBexyUWcCbSjC8G5LfptR0TS2nKU5oLDgPCBXk7vFC0yP0yeQBVCq7AwiRcJRvrgFSSJ4KqTCWcFqASzZLJ6PYm8TtuUkqhBRM52X6zVgOy1LpMHGe1WnVW3Y6QC8d3Xc%2F5%2FfZmwpZQUGJdgdIZp7oGuQ3PBaP5Uijt%2FDq6fTQFHi8F58C0kHegrx1KmepQVa6JNb4akCy1w27kRj0v7rluHHleEEZe0I17YRB3%2BxjEm36mYkAeGcy7bBa5duy5gR0wd2bHaRrazAv6EaPhPEgR2VipCsbcUKQHxHf90HZD23enXj8JeokXdmK394FY7xuKkQQy%2FPHNDxsOk%2Fp7uU%2Fgaf6oUiAND8S6FrKg%2BnS4sWDj8zo0Aa4z%2FUyGe4SLT5p2mCgcWH%2Fyn0q6iP7sxn88FM%2FTMI0unH2QL6jLZKKprpSxtEyXIgXrPc0rOA1M1dHJpGIMlCJOndw5yr5ladR0%2FVVEbdWPfb%2FXDyK%2F6wZ9PAhhL%2FK9KHY9P%2Br77jco2ZLyv5ClgTCpZh9xAGpTY7vDWuOrV2F5Ha8Nq6BZPkpTaQQa0hw%2B4hqQovMZZjTPhfppYQIMygbQptRB9S0inMx5ZrIaDW9BL0V6miFWJDOgEuSG4lP5rqimXze8d0K%2F4%2B%2FkaK5BtiUPvY3kEUr%2BACwrMzDn4p8vH2fLh%2FPFBjYKOscSbnvG2DQzgcog%2FhlQHzg6oXtwz2zrQKdRlWKDDJBFLTO2g3UUMJzcY8sN2p31oMcvJWv8Lw3ttzmq9JKb0YcCqbbq1zPmcYLHE1ONeQrrc5VvtY6FEJKGtW73%2FOK5zHGjPMB8eHLpsISZODTf42MlZHqPFx7qCelUUo4nVOoXlv4me4vHFrIDx46qAxI1Mj6rNBx6j9yWmVUcQVySv5gxJrXhnN1FMYWxNmtihnGsNaW7MvVNsLvqs9fvejurFWeAPwgqS%2FRziSjXuPExIV%2Bct4YOq7cobXyv0nKdSaXN6%2FdCzWKdwtO%2Fy8GNWGT8%2F3k0jqwHA7XzNn8ZaNz9vjQ30fAv&RelayState=http%3A%2F%2Frelaystate.com&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=aeNkIQLkkrNIxVgd1slzZXJpkEzvU0LIwqMR9wWLRT%2FjMHo7ldaCeGlFk3H%2Bbr4l3qEttjsTBWgTGgPDgzax7DDCUSvJPdAh0YB14T9oZ243cxap2OOi483TkBPt%2BwM6Q4AaePWbH1NdUvFUmP9ovl4Ub3iC4O%2FmZFRR3l4TU4z5ZR5OO8%2FFm%2BppvYXf%2FJDbsTLkKgF72a1lD1YhNWdqYKx3%2BQ22x94osmXis3omG7cdNDlo8ULesWL2RVXzftjmHa9zqWidTrHjyA6fSouTV3pQHmzrI8t9g3tuk5jKzTbOPmF2KBhEPzvN26jH2Bdy5b4PCvkJ1L9VeJKlGwBejQ%3D%3D"; + RedirectBinding redirectBindingLoginResponse = new RedirectBinding(); + redirectBindingLoginResponse.init(loginResponse); + Assert.assertTrue("testIsLogout Logout", redirectBindingLogoutResponse.isLogout()); + Assert.assertFalse("testIsLogout Login", redirectBindingLoginResponse.isLogout()); + } + + @Test + public void testGetRelayState() { + String expected = "http://relaystate.com"; + Assert.assertEquals(expected, redirectBindingLogoutResponse.getRelayState()); + } + + @Test + public void testLoginRequest() { + String function = "Login"; + RedirectBinding redirectBinding = new RedirectBinding(); + SamlParms parms = createParameters(); + String samlRequest = redirectBinding.login(parms, "http://relaystate.com"); + String queryString = getQueryString(samlRequest); + Map redirectMessage = parseRedirect(queryString); + + //test login request parameters + testRequestParameters(redirectMessage, function); + + //test login signature + testRequestSignature(redirectMessage, function); + + //test login request xml parameters + String xml = Encoding.decodeAndInflateXmlParameter(redirectMessage.get("SAMLRequest")); + String expectedXml = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"; + Assert.assertEquals("Test Login request xml parameters", expectedXml, xml); + + } + + @Test + public void testLogoutRequest() { + String function = "Logout"; + RedirectBinding redirectBinding = new RedirectBinding(); + SamlParms parms = createParameters(); + String samlRequest = redirectBinding.logout(parms, "http://relaystate.com"); + String queryString = getQueryString(samlRequest); + Map redirectMessage = parseRedirect(queryString); + + //test logout request parameters + testRequestParameters(redirectMessage, function); + + //test logout signature + testRequestSignature(redirectMessage, function); + + //test logout request xml parameters + String xml = Encoding.decodeAndInflateXmlParameter(redirectMessage.get("SAMLRequest")); + String expectedXml = "SPEntityIDnameID123456789"; + Assert.assertEquals("Test Logout request xml parameters", expectedXml, xml); + + } + + private void testRequestParameters(Map redirectMessage, String function) { + String relayState = decodeParm(redirectMessage.get("RelayState")); + Assert.assertEquals(MessageFormat.format("Test {0} parameters RelayState", function), "http://relaystate.com", relayState); + String sigAlg = decodeParm(redirectMessage.get("SigAlg")); + Assert.assertEquals(MessageFormat.format("Test {0} request parameters SigAlg", function), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", sigAlg); + } + + private void testRequestSignature(Map redirectMessage, String function) { + boolean verifies = verifySignature_internal(resources.concat("/mykeystore.jks"), password, alias, redirectMessage); + Assert.assertTrue(MessageFormat.format("Test {0} request signature", function), verifies); + } + + private static String getIssuerInstant(String xml, String name) { + Document doc = SamlAssertionUtils.canonicalizeXml(xml); + return doc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", name).item(0).getAttributes().getNamedItem("IssueInstant").getNodeValue(); + + } + + private static String decodeParm(String parm) { + try { + return URLDecoder.decode(parm, StandardCharsets.UTF_8.name()); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + private static String getQueryString(String samlRequest) { + try { + java.net.URL url = new java.net.URL(samlRequest); + return url.getQuery(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + private static SamlParms createParameters() { + SamlParms parms = new SamlParms(); + parms.setCertPath(resources.concat("/mykeystore.jks")); + parms.setCertPass(password); + parms.setCertAlias(alias); + parms.setAcs("http://myapp.com/acs"); + parms.setForceAuthn(false); + parms.setServiceProviderEntityID("EntityID"); + parms.setServiceProviderEntityID("SPEntityID"); + parms.setPolicyFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); + parms.setAuthnContext("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + parms.setEndPointLocation("http://endpoint/saml"); + parms.setId("_idtralala"); + parms.setSingleLogoutEndpoint("http://idp.com/slo"); + parms.setSessionIndex("123456789"); + parms.setServiceProviderEntityID("SPEntityID"); + parms.setNameID("nameID"); + return parms; + } + + + private static Map parseRedirect(String request) { + Map result = new HashMap<>(); + String[] redirect = request.split("&"); + + for (String s : redirect) { + String[] res = s.split("="); + result.put(res[0], res[1]); + } + return result; + } + + private static boolean verifySignature_internal(String certPath, String certPass, String certAlias, Map redirectMessage) { + + + byte[] signature = Encoding.decodeParameter(redirectMessage.get("Signature")); + + String signedMessage; + if (redirectMessage.containsKey("RelayState")) { + signedMessage = MessageFormat.format("SAMLRequest={0}", redirectMessage.get("SAMLRequest")); + signedMessage += MessageFormat.format("&RelayState={0}", redirectMessage.get("RelayState")); + signedMessage += MessageFormat.format("&SigAlg={0}", redirectMessage.get("SigAlg")); + } else { + signedMessage = MessageFormat.format("SAMLRequest={0}", redirectMessage.get("SAMLRequest")); + signedMessage += MessageFormat.format("&SigAlg={0}", redirectMessage.get("SigAlg")); + } + + byte[] query = signedMessage.getBytes(StandardCharsets.UTF_8); + + X509Certificate cert = Keys.loadCertificate(certPath, certAlias, certPass); + + try (InputStream inputStream = new ByteArrayInputStream(query)) { + String sigalg = URLDecoder.decode(redirectMessage.get("SigAlg"), StandardCharsets.UTF_8.name()); + RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(Hash.getHashFromSigAlg(sigalg))); + setUpSigner(signer, inputStream, Keys.getAsymmetricKeyParameter(cert), false); + return signer.verifySignature(signature); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private static void setUpSigner(Signer signer, InputStream input, AsymmetricKeyParameter asymmetricKeyParameter, boolean toSign) { + + byte[] buffer = new byte[8192]; + int n; + try { + signer.init(toSign, asymmetricKeyParameter); + while ((n = input.read(buffer)) > 0) { + signer.update(buffer, 0, n); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/gamsaml20/src/test/resources/keystore.jks b/gamsaml20/src/test/resources/keystore.jks new file mode 100644 index 000000000..7b0d9e9f9 Binary files /dev/null and b/gamsaml20/src/test/resources/keystore.jks differ diff --git a/gamsaml20/src/test/resources/mykeystore.jks b/gamsaml20/src/test/resources/mykeystore.jks new file mode 100644 index 000000000..f859734c8 Binary files /dev/null and b/gamsaml20/src/test/resources/mykeystore.jks differ diff --git a/pom.xml b/pom.xml index 400cf850e..88220bfce 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,7 @@ gxcloudstorage-awss3-v2 gxcompress securityapicommons + gamsaml20 gxjwt gxcryptography gxxmlsignature