Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save paulsena/10123799 to your computer and use it in GitHub Desktop.
Save paulsena/10123799 to your computer and use it in GitHub Desktop.
Modified version of SSO SAML 2.0 Update 1 Script Include. Adds support for HTTP POST on AuthNRequests. Out of box ServiceNow just supports HTTP Redirection when sending Auth Requests from SN to the Identity Provider. Added a new function called generateAuthnRequestForm. Call this function from the Installation Exist Script instead of generateAut…
gs.include("PrototypeServer");
var SAML2Error = Class.create();
SAML2Error.prototype = Object.extend(new Error(), {
initialize : function(message) {
this.name = "SAML2Error";
this.message = message;
}
});
var SAML2ValidationError = Class.create();
SAML2ValidationError.prototype = Object.extend(new SAML2Error(), {
initialize : function(message) {
this.name = "SAML2ValidationError";
this.message = message;
}
});
var SAML2_update1 = Class.create();
SAML2_update1.prototype = {
initialize : function() {
// set the following system property to control Debug logging for SAML
this.debug = gs.getProperty("glide.authenticate.sso.saml2.debug", "false");
this.serviceUrl = gs.getProperty("glide.authenticate.sso.saml2.service_url");
this.clockskew = new GlideBoundedIntProperty("glide.authenticate.sso.saml2.clockskew", 60, 0, 3600).getInt();
this.lastGeneratedRequestID = null;
this.inResponseTo = null;
this.logoutFailureEventId = "saml2.logout.validation.failed";
this.certGR = this.getCertGR();
// Keep SAMLAssertion object for validation
this.SAMLResponseObject = null;
this.SAMLAssertion = null;
// Initialize OpenSAML library.
var DefaultBootstrap = Packages.org.opensaml.DefaultBootstrap;
DefaultBootstrap.bootstrap();
},
/*
* Returns SAML 2.0 Certificate GlideRecord from sys_certificate
*/
getCertGR : function() {
var gr = new GlideRecord("sys_certificate");
gr.addQuery("name", "SAML 2.0");
gr.addActiveQuery();
gr.query();
if (gr.next())
return gr;
else
return null;
},
validateLoginResponse : function(samlResponseObject, inResponseTo) {
try {
this.inResponseTo = inResponseTo;
this.SAMLResponseObject = samlResponseObject;
if(!this.SAMLResponseObject)
throw new SAML2Error("Unable to retrieve SAMLResponseObject");
this.logDebug("Signature Reference ID: " + this.SAMLResponseObject.getSignatureReferenceID().toString());
// get SAMLAssertion object
this.SAMLAssertion = this.SAMLResponseObject.getAssertions().get(0);
if (!this.SAMLAssertion)
throw new SAML2Error("Unable to retrieve SAML Assertion.");
this.validateSignature();
this.validateCertificate();
this.validateConditions();
this.validateIssuer();
this.validateSubjectConfirmations();
this.logDebug("Authn response object validated.");
return true;
} catch (e) {
if (e instanceof Packages.java.lang.Throwable)
this.logDebug(e.getMessage());
else
this.logDebug(e.name + ": " + e.message);
return false;
}
},
validateLogoutResponseObject : function(samlResponseObject, inResponseTo) {
try {
if (!samlResponseObject)
throw new SAML2ValidationError("Failed to validate logout response. samlResponseObject is null.");
var inResponseToValue = samlResponseObject.getInResponseTo();
var statusValue = samlResponseObject.getStatus().getStatusCode().getValue();
if (inResponseTo && !inResponseToValue.equals(inResponseTo))
throw new SAML2ValidationError("Failed to validate logout response. Expected: " + inResponseTo + ", actual: "
+ inResponseToValue);
if (!statusValue || !statusValue.equals("urn:oasis:names:tc:SAML:2.0:status:Success"))
throw new SAML2ValidationError(
"Failed to validate logout response status. Expected: urn:oasis:names:tc:SAML:2.0:status:Success, actual: "
+ statusValue);
this.logDebug("SAML2 LogoutResponse validated.");
return true;
} catch (e) {
if (e instanceof Packages.java.lang.Throwable)
this.logDebug(e.getMessage());
else
this.logDebug(e.name + ": " + e.message);
return false;
}
},
createSAMLResponseObject : function(SAMLResponseXML) {
if(!SAMLResponseXML || SAMLResponseXML.equals(""))
return null;
this.logDebug("SAML Response xml: " + SAMLResponseXML);
var Configuration = Packages.org.opensaml.xml.Configuration;
var XMLUtil = GlideXMLUtil;
var document = XMLUtil.parse(SAMLResponseXML, true);
document.normalizeDocument();
var metadataRoot = document.getDocumentElement();
// get an unmarshaller
var unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(metadataRoot);
// unmarshall using the document root element
var SAMLResponseObject = unmarshaller.unmarshall(metadataRoot);
if (!SAMLResponseObject)
throw new SAML2Error("Unable to unmarshall response");
// we have the xml unmarshalled to a response object
this.logDebug("Response object created");
this.logDebug("Issue Instant: " + SAMLResponseObject.getIssueInstant().toString());
return SAMLResponseObject;
},
generateURLEncodedSignatureForSignedQueryString: function(signingCredential, algorithmURI, queryString) {
var Base64 = Packages.org.opensaml.xml.util.Base64;
var SigningUtil = Packages.org.opensaml.xml.security.SigningUtil;
var JavaString = Packages.java.lang.String;
var b64Signature = null;
var rawSignature = SigningUtil.signWithURI(signingCredential, algorithmURI, new JavaString(queryString).getBytes("UTF-8"));
b64Signature = Base64.encodeBytes(rawSignature, Base64.DONT_BREAK_LINES);
var uriEncodedQuerySig = encodeURIComponent(b64Signature);
return uriEncodedQuerySig;
},
generateAuthnRequestRedirectURL : function(request) {
var baseurl = gs.getProperty("glide.authenticate.sso.saml2.idp_authnrequest_url");
var authReqStr = this.getAuthnRequestString();
var relayState = this.generateRelayState(request);
var queryString = "SAMLRequest=" + authReqStr + "&RelayState=" + relayState;
var signAuthn = gs.getProperty("glide.authenticate.sso.saml2.require_signed_authnrequest", false);
var authReqSig = null;
if(this.isTrue(signAuthn)) {
var algorithmURI = gs.getProperty('glide.authenticate.sso.saml2.sign_algorithmURI');
queryString = queryString + "&SigAlg=" + encodeURIComponent(algorithmURI);
authReqSig = this.generateURLEncodedSignatureForSignedQueryString(this.generateCredential(), algorithmURI, queryString);
queryString = queryString + "&Signature="+ authReqSig;
}
var redirectURL = this.constructURL(baseurl, queryString);
this.logDebug("Redirecting to: " + redirectURL);
request.getSession().setAttribute("glide.saml2.session_request_id", this.getLastGeneratedRequestID());
return redirectURL;
},
// **** Start - Fruition Partners Modification ****
/*
* New Function to generate an AuthnRequest via HTTP POST instead of HTTP GET (Redirection)
* No Signature, Deflate, or URL Encoding
*/
generateAuthnRequestForm: function (request) {
var AuthnRequestMarshaller = Packages.org.opensaml.saml2.core.impl.AuthnRequestMarshaller;
var authnRequest = this.createAuthnRequestWithOptions(null);
var signAuthn = gs.getProperty("glide.authenticate.sso.saml2.require_signed_authnrequest", false);
if(this.isTrue(signAuthn))
this.signSAMLObject(authnRequest);
var arm = new AuthnRequestMarshaller();
var samlRequestElement = arm.marshall(authnRequest);
var samlRequest = this.getEncodedSAMLRequest(samlRequestElement, false, false);
var idpLoginURL = gs.getProperty("glide.authenticate.sso.saml2.idp_authnrequest_url");
var relayState = this.generateRelayState(request, true);
request.getSession().setAttribute("glide.saml2.session_request_id", this.getLastGeneratedRequestID());
return this.createSAMLRequestPostForm(idpLoginURL, samlRequest, relayState);
},
// **** End - Modification ****
getAuthnRequestString : function() {
var AuthnRequestMarshaller = Packages.org.opensaml.saml2.core.impl.AuthnRequestMarshaller;
var authnRequest = this.createAuthnRequestWithOptions(null);
var arm = new AuthnRequestMarshaller();
var elem = arm.marshall(authnRequest);
return this.getEncodedSAMLRequest(elem, true, true);
},
generateLogoutRequestURL : function(request) {
var samlRequestElement = this.getLogoutRequestElement(request);
var samlRequest = this.getEncodedSAMLRequest(samlRequestElement, true, true);
var idpLogoutURL = gs.getProperty("glide.authenticate.sso.saml2.idp_logout_url");
var queryString = "SAMLRequest=" + samlRequest + "&RelayState=" + this.serviceUrl;
var redirectURL = this.constructURL(idpLogoutURL, queryString);
return redirectURL;
},
constructURL : function (baseurl, queryString) {
if(!queryString)
return baseurl;
if(baseurl && baseurl.indexOf('?') > -1)
baseurl = baseurl + '&';
else
baseurl = baseurl + '?';
var url = baseurl + queryString;
return url;
},
getLogoutRequestElement : function(request) {
var LogoutRequestMarshaller = Packages.org.opensaml.saml2.core.impl.LogoutRequestMarshaller;
var logoutRequest = this.createLogoutRequest(request);
var signLogout = gs.getProperty("glide.authenticate.sso.saml2.require_signed_logoutrequest", false);
if(this.isTrue(signLogout))
this.signSAMLObject(logoutRequest);
var lrm = new LogoutRequestMarshaller();
var elem = lrm.marshall(logoutRequest);
return elem;
},
generateLogoutRequestForm : function(request) {
var samlRequestElement = this.getLogoutRequestElement(request);
var samlRequest = this.getEncodedSAMLRequest(samlRequestElement, false, false);
var idpLogoutURL = gs.getProperty("glide.authenticate.sso.saml2.idp_logout_url");
var relayState = this.serviceUrl;
return this.createSAMLRequestPostForm(idpLogoutURL, samlRequest, relayState);
},
createSAMLRequestPostForm : function(actionurl, samlrequest, relaystate) {
if(!actionurl)
throw new SAML2Error("createSAMLRequestPostForm: IDP URL can not be null.");
var output = '<html lang="en">';
output += '<body onload="document.forms[0].submit()">';
output += '<form method="POST" action="' + actionurl +'">';
output += '<input type="HIDDEN" name="SAMLRequest" value="' + samlrequest +'" />';
output += '<input type="HIDDEN" name="RelayState" value="'+ relaystate +'" />';
output += '</form>';
output += '</body>';
output += '</html>';
this.logDebug("html post: "+output);
return output;
},
/*
* Validate validity of the stored certificate. Then, validate
* Signature with stored certificate and SignatureProfile.
*/
validateSignature : function() {
var BasicX509Credential = Packages.org.opensaml.xml.security.x509.BasicX509Credential;
var SignatureValidator = Packages.org.opensaml.xml.signature.SignatureValidator;
var SignatureProfileValidator = Packages.org.opensaml.security.SAMLSignatureProfileValidator;
var Document = Packages.org.w3c.dom.Document;
var Element = Packages.org.w3c.dom.Element;
var Node = Packages.org.w3c.dom.Node;
var StringReader = Packages.java.io.StringReader;
var QName = Packages.javax.xml.namespace.QName;
var X509Certificate = Packages.javax.security.cert.X509Certificate;
var XMLUtil = GlideXMLUtil;
var Date = Packages.java.util.Date;
if (!this.SAMLResponseObject || !this.SAMLAssertion)
throw new SAML2ValidationError("SAMLResponseObject or SAMLAssertion is null.");
var certificate = null;
if (this.certGR != null) {
var bytes;
var str = this.certGR.pem_certificate.toString();
str = str.substring(28, str.indexOf('-----END CERTIFICATE-----')).trim();
bytes = GlideStringUtil.base64DecodeAsBytes(str);
certificate = X509Certificate.getInstance(bytes);
} else {
throw new SAML2ValidationError("Unable to locate SAML 2.0 certificate.");
}
this.logDebug("certificate Issuer DN: " + certificate.getIssuerDN().getName());
this.logDebug("certificate valid date from: " + certificate.getNotBefore().toString());
this.logDebug("certificate valid date to: " + certificate.getNotAfter().toString());
this.logDebug("Current timestamp: " + (new Date()).toString());
try {
certificate.checkValidity();
} catch (ve) {
throw new SAML2ValidationError(ve.getMessage());
}
// generate public key to validate signatures
var publicKey = certificate.getPublicKey();
if (publicKey != null)
// we have the public key
this.logDebug("Public key created");
else
throw new SAML2ValidationError("Public key not found in certificate");
// create credentials
var publicCredential = new BasicX509Credential();
// add public key value
publicCredential.setPublicKey(publicKey);
// create SAMLProfileSignatureValidator
var signatureProfileValidator = new SignatureProfileValidator();
// create SignatureValidator
var signatureValidator = new SignatureValidator(publicCredential);
// get the signature to validate from the response object
var signature = this.SAMLResponseObject.getSignature();
if (!signature) {
this.logDebug("Signature not in response, attempting to get signature from assertion");
signature = this.SAMLAssertion.getSignature(); // get signature from assertion as 2nd attempt
if (!signature)
throw new SAML2ValidationError("Signature not found in response");
else
this.logDebug("Got signature");
}
this.logDebug(XMLUtil.toString(signature.getDOM()));
// validate signature profile and signature
try {
signatureProfileValidator.validate(signature);
signatureValidator.validate(signature);
} catch (ve) {
throw new SAML2ValidationError(ve.getMessage());
}
this.logDebug("Signature validated.");
},
/*
* Validate <Issuer> element within Assertion
* see SAML2 core specification, section 2.3.3
* http://www.oasis-open.org/specs/index.php#samlv2.0
*
* Further, SNC requires following:
* 1) OneTimeUse condition must not present. It is currently not supported.
* 2) ProxyRestriction condition must not present. It is currently not supported.
*/
validateIssuer : function() {
var issuer = this.SAMLAssertion.getIssuer().getValue();
var idp = gs.getProperty("glide.authenticate.sso.saml2.idp");
if (issuer.equals(idp) == false)
throw new SAML2ValidationError("Assertion issuer is invalid. Expect: " + idp + ", actual: " + issuer);
this.logDebug("Issuer validated.");
},
/*
* Validate <Conditions> element.
* see SAML2 core specification, section 2.5.
* http://www.oasis-open.org/specs/index.php#samlv2.0
*/
validateConditions : function() {
var conditions = this.SAMLAssertion.getConditions();
if (!conditions)
throw new SAML2ValidationError("Assertion/Conditions must present.");
var DateTime = Packages.org.joda.time.DateTime;
var List = Packages.java.util.List;
var AudienceRestriction = Packages.org.opensaml.saml2.core.AudienceRestriction;
var OneTimeUse = Packages.org.opensaml.saml2.core.OneTimeUse;
var ProxyRestriction = Packages.org.opensaml.saml2.core.ProxyRestriction;
var now = new DateTime();
var notBefore = conditions.getNotBefore();
var notOnOrAfter = conditions.getNotOnOrAfter();
if (notBefore && !now.isAfter(notBefore.minusSeconds(this.clockskew)))
throw new SAML2ValidationError("Assertion is valid in the future, now: " + now + ", notBefore: " + notBefore);
if (notOnOrAfter && !now.isBefore(notOnOrAfter.plusSeconds(this.clockskew)))
throw new SAML2ValidationError("Assertion is expired, now: " + now + ", notOnOrAfter: " + notOnOrAfter);
// Validate each of AudienceRestriction condition.
// There can be more than one <AudienceRestriciton> element, every one of those must be valid.
var list = conditions.getConditions();
var condition = null;
var size = list.size();
if (size <= 0)
throw new SAML2ValidationError("No Condition or AudienceRestriction is found in Assertion/Conditions.");
for ( var i = 0; i < size; i++) {
condition = list.get(i);
if (condition instanceof AudienceRestriction)
this._validateAudienceRestriction(condition);
else if (condition instanceof OneTimeUse)
throw new SAML2ValidationError("OneTimeUse condition is not supported.");
else if (condition instanceof ProxyRestriction)
throw new SAML2ValidationError("ProxyRestriction condition is not supported.");
else
throw new SAML2ValidationError("Not understood condition found.");
}
this.logDebug("Conditions validated.");
},
/*
* Validate <AudienceRestriction> element.
* see SAML2 core specification, section 2.5.1.
* http://www.oasis-open.org/specs/index.php#samlv2.0
*/
_validateAudienceRestriction : function(audienceRestriction) {
if (!audienceRestriction)
throw new SAML2ValidationError("Condition audienceRestriction is null.");
var propAudience = gs.getProperty("glide.authenticate.sso.saml2.audience");
var audienceList = audienceRestriction.getAudiences();
var audience = null;
var audienceUri = null;
var size = audienceList.size();
for ( var i = 0; i < size; i++) {
audience = audienceList.get(i);
audienceUri = audience.getAudienceURI();
if (audience && audienceUri.equals(propAudience) == true) {
this.logDebug("Found matching audience.");
return;
}
else
this.logDebug("Assertion audience mismatch. Expect: " + propAudience + ", actual: " + audienceUri);
}
throw new SAML2ValidationError("AudienceRestriction validation failed. No matching audience found.");
},
/*
* Validate <SubjectConfirmation> element.
* see SAML2 core specification, section 2.4.1
* http://www.oasis-open.org/specs/index.php#samlv2.0
*
* Further, SNC requires following:
* 1) At least one valid <SubjectConfirmation> must present.
*/
validateSubjectConfirmations : function() {
var subject = this.SAMLAssertion.getSubject();
if (!subject)
throw new SAML2ValidationError("Subject must present.");
var subjectConfirmationList = subject.getSubjectConfirmations();
if (!subjectConfirmationList)
throw new SAML2ValidationError("SubjectConfirmation must present.");
var subjectConfirmation = null;
var size = subjectConfirmationList.size();
for ( var i = 0; i < size; i++) {
subjectConfirmation = subjectConfirmationList.get(i);
try {
this._validateSubjectConfirmation(subjectConfirmation);
this.logDebug("SubjectConfirmations validated.");
return; // any valid subjectConfirmation is sufficient to confirm the subject.
} catch (e) {
if (e instanceof SAML2ValidationError)
this.logDebug(e.name + ": " + e.message);
else
throw e;
}
}
throw new SAML2ValidationError("No valid SubjectConfirmation found.");
},
/*
* Validate <SubjectConfirmation> element.
* see SAML2 core specification, section 2.4.1.1.
* http://www.oasis-open.org/specs/index.php#samlv2.0
*
* Further, SNC requires following:
* 1) Method attribute in <SubjectConfirmation> must be urn:oasis:names:tc:SAML:2.0:cm:bearer
* see: SAML2 profile specification, section 3.3
* http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf
* 2) Recipient attribute in <SubjectConfirmationData> must present.
* 3) Address attribute in <SubjectConfirmationData> is ignored. It is currently not supported.
* 4) "Any" attributes is not supported.
*
*/
_validateSubjectConfirmation : function(subjectConfirmation) {
var method = subjectConfirmation.getMethod();
if (!method || method.equals("urn:oasis:names:tc:SAML:2.0:cm:bearer") == false)
throw new SAML2ValidationError("SubjectConfirmation method: " + method + " is not supported.");
var DateTime = Packages.org.joda.time.DateTime;
var subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData();
var now = new DateTime();
var notBefore = subjectConfirmationData.getNotBefore();
var notOnOrAfter = subjectConfirmationData.getNotOnOrAfter();
var inResponseTo = subjectConfirmationData.getInResponseTo();
var recipient = subjectConfirmationData.getRecipient();
var address = subjectConfirmationData.getAddress();
var unknownAttributeMap = subjectConfirmationData.getUnknownAttributes();
var consumerServiceURL = gs.getSession().getHttpSession().getAttribute("glide.saml2.assertion_consumer_service_url");
if (!consumerServiceURL)
consumerServiceURL = this.serviceUrl;
// Optional attributes.
if (notBefore && !now.isAfter(notBefore.minusSeconds(this.clockskew)))
throw new SAML2ValidationError("Subject is valid in the future. Now: "
+ now.toString() + ", NotBefore: " + notBefore.toString());
if (notOnOrAfter && !now.isBefore(notOnOrAfter.plusSeconds(this.clockskew)))
throw new SAML2ValidationError("Subject is expired. Now: "
+ now.toString() + ", NotOnOrAfter: " + notOnOrAfter.toString());
if (inResponseTo && !inResponseTo.equals(this.inResponseTo))
throw new SAML2ValidationError("InResponseTo attribute in SubjectConfirmationData mismatch. Expect: "
+ this.inResponseTo + ", actual: " + inResponseTo);
if (recipient && !recipient.equals(consumerServiceURL))
throw new SAML2ValidationError("Recipient attribute in SubjectConfirmationData mismatch. Expect: "
+ this.serviceUrl + ", actual: " + recipient);
// We don't support "Any" attribute extension.
if (unknownAttributeMap && unknownAttributeMap.size() > 0)
throw new SAML2ValidationError("Unknown attribute in SubjectConfirmationData is not supported.");
},
/*
* Verify the certificates matches what is stored in the database.
* KeyInfo is an optional element in the signature. So, the comparison is skipped if there is no keyInfo.
*/
validateCertificate : function() {
// Verify that assertion is authorized by comparing the installed certificate
// with the one posted in the SAMLResponse
if (!this.certGR)
throw new SAML2ValidationError("Could not find a digital signature stored in Service Now instance.");
var certStr = this.certGR.pem_certificate.toString();
certStr = certStr.substring(28, certStr.indexOf('-----END CERTIFICATE-----')).trim();
certStr = Packages.org.apache.commons.lang.StringUtils.deleteWhitespace(certStr);
var signature = this.SAMLResponseObject.getSignature();
if (!signature) {
this.logDebug("Signature not in response, attempting to get signature from assertion");
signature = this.SAMLAssertion.getSignature(); // get signature from assertion as 2nd attempt
if (!signature)
throw new SAML2ValidationError("Signature not found in response");
}
var keyInfo = signature.getKeyInfo();
if (!keyInfo) {
this.logDebug("The optional keyInfo field is missing. Skipping the certificate validation.");
return;
}
var inboundCert = keyInfo.getX509Datas().get(0).getX509Certificates().get(0).getValue();
inboundCert = Packages.org.apache.commons.lang.StringUtils.deleteWhitespace(inboundCert);
if (!inboundCert)
throw new SAML2ValidationError("Could not find X509Certificate in the inbound SAMLResponse");
if (certStr.equals(inboundCert) == false)
throw new SAML2ValidationError("Certificates don't match. Expect: " + certStr + ", actual: " + inboundCert);
this.logDebug("Certificate validated.");
},
getSubjectNameID : function() {
var nameId = null;
try {
nameId = this.SAMLAssertion.getSubject().getNameID().getValue();
} catch (e) {
this.logDebug("Subject NameID value not found:" + e.getMessage());
}
this.logDebug("Subject NameID:" + nameId);
return nameId;
},
getSessionIndex : function() {
var sessionIndex = null;
try {
sessionIndex = this.SAMLAssertion.getAuthnStatements().get(0).getSessionIndex();
} catch (e) {
this.logDebug("SessionIndex value not found:" + e.getMessage());
}
this.logDebug("SessionIndex: " + sessionIndex);
return sessionIndex;
},
/*
* Return Base64 encoded xml string from given xml element.
* needDeflate: if true, this will deflate xml string before base64 encode.
*/
getEncodedSAMLRequest : function(element, needDeflate, needUrlEncode) {
var XMLUtil = GlideXMLUtil;
var ByteArrayOutputStream = Packages.java.io.ByteArrayOutputStream;
var Deflater = Packages.java.util.zip.Deflater;
var DeflaterOutputStream = Packages.java.util.zip.DeflaterOutputStream;
var Base64 = Packages.org.opensaml.xml.util.Base64;
var StringUtil = GlideStringUtil;
var samlRequest = XMLUtil.toFragmentString(element.getOwnerDocument());
var requestBytes = samlRequest.getBytes("UTF-8");
this.logDebug("SAML Request xml: " + samlRequest);
if (!needDeflate) {
return Base64.encodeBytes(requestBytes, Base64.DONT_BREAK_LINES);
}
var bytesOut = new ByteArrayOutputStream();
var deflater = new Deflater(Deflater.DEFLATED, true);
var deflaterStream = new DeflaterOutputStream(bytesOut, deflater);
deflaterStream.write(requestBytes, 0, requestBytes.length);
deflaterStream.finish();
var base64EncodedSamlRequest = Base64.encodeBytes(bytesOut.toByteArray(), Base64.DONT_BREAK_LINES);
if (needUrlEncode)
base64EncodedSamlRequest = StringUtil.urlEncode(base64EncodedSamlRequest);
return base64EncodedSamlRequest;
},
/*
* Take SAMLResponse out of HttpRequest parameter and decoded.
* Returns response xml string.
*/
getDecodedSAMLResponse : function(request) {
var ByteArrayOutputStream = Packages.java.io.ByteArrayOutputStream;
var Inflater = Packages.java.util.zip.Inflater;
var InflaterOutputStream = Packages.java.util.zip.InflaterOutputStream;
var StringUtil = GlideStringUtil;
var String = Packages.java.lang.String;
var encodedResponse = request.getParameter("SAMLResponse"); //already url-decoded.
if(!encodedResponse || encodedResponse.equals(""))
return null;
var requestBytes = StringUtil.base64DecodeAsBytes(encodedResponse);
if (request.getMethod().equals("POST"))
return new String(requestBytes, "UTF-8");
// else, it's coming from HTTP-Redirect which was deflated and we need to
// inflate it back.
var inflater = new Inflater(true);
var bos = new ByteArrayOutputStream();
var ios = new InflaterOutputStream(bos, inflater);
try {
ios.write(requestBytes, 0, requestBytes.length);
var outputString = bos.toString("UTF-8");
ios.close();
return outputString;
} catch (e) {
ios.close();
throw e;
}
},
getSAMLObjectFromRequest : function (request) {
try{
var samlXML = this.getDecodedSAMLResponse(request);
var samlObject = this.createSAMLResponseObject(samlXML);
return samlObject;
} catch (e) {
if (e instanceof Packages.java.lang.Throwable)
this.logDebug(e.getMessage());
else
this.logDebug(e.name + ": " + e.message);
}
return null;
},
isLogoutResponse : function(samlResponseObject) {
return (samlResponseObject instanceof Packages.org.opensaml.saml2.core.LogoutResponse );
},
createLogoutRequest : function(request) {
var LogoutRequestBuilder = Packages.org.opensaml.saml2.core.impl.LogoutRequestBuilder;
var DateTime = Packages.org.joda.time.DateTime;
var SAMLVersion = Packages.org.opensaml.common.SAMLVersion;
var b = new LogoutRequestBuilder();
var r = b.buildObject();
r.setID(this.generateRequestID());
r.setVersion(SAMLVersion.VERSION_20);
r.setIssueInstant(new DateTime());
r.setIssuer(this.createIssuer());
r.setNameID(this.createNameID(request));
var idpLogoutURL = gs.getProperty("glide.authenticate.sso.saml2.idp_logout_url");
r.setDestination(idpLogoutURL);
r.getSessionIndexes().add(this.createSessionIndex(request));
return r;
},
createSessionIndex : function(request) {
var SessionIndexBuilder = Packages.org.opensaml.saml2.core.impl.SessionIndexBuilder;
var sessionIndex = request.getSession().getAttribute("glide.saml2.session_index");
var sib = new SessionIndexBuilder();
var si = sib.buildObject();
si.setSessionIndex(sessionIndex);
return si;
},
createNameID : function(request) {
var NameIDBuilder = Packages.org.opensaml.saml2.core.impl.NameIDBuilder;
var nameIdPolicy = gs.getProperty("glide.authenticate.sso.saml2.nameid_policy");
var serviceURL = gs.getProperty("glide.authenticate.sso.saml2.service_url");
var nameId = request.getSession().getAttribute("glide.saml2.session_id");
if (!nameIdPolicy || nameIdPolicy.equals("")) {
this.logDebug("No name ID policy configured, skipping optional specification");
return null;
}
var nb = new NameIDBuilder();
var nid = nb.buildObject();
nid.setValue(nameId);
nid.setFormat(nameIdPolicy);
return nid;
},
/*
* Generate proper RelayState parameter value.
* This should mostly preserve original user request URL so that we can land on
* the right page after authentication.
*/
generateRelayState : function(request, skipURIEncoding) {
// Set up base url
var urlTokens = /^(http(s?)\:\/\/.*)\/\S+$/(this.serviceUrl);
var baseUrl = this.serviceUrl;
if (urlTokens && urlTokens.length > 1)
baseUrl = urlTokens[1];
this.logDebug("Stripping down the serviceURL: " + this.serviceUrl + " to a base URL of: " + baseUrl);
// grab the request URI and query string from the request
var requestURI = request.getRequestURI();
this.logDebug("requestURI: " + requestURI);
var qs = request.getQueryString();
this.logDebug("Query String (qs): " + qs);
if (!requestURI ||requestURI.equals("") || requestURI.equals("/")) {
// No deep linking
this.logDebug("No Deep Linking for this SAML request");
relayState = this.serviceUrl;
} else if (this.needNavFrame(request)) {
this.logDebug("There may be Deep Linking involved with this SAML request");
//use saml_redirector to make sure we bust out of the current frame before loading another NavFrame.
if (qs && !qs.equals(""))
requestURI = requestURI + '?' + qs;
relayState = baseUrl + "/saml_redirector.do?sysparm_nostack=true&sysparm_uri="+ encodeURIComponent("/nav_to.do?uri=" + encodeURIComponent(requestURI));
} else {
relayState = baseUrl + requestURI;
if (qs && !qs.equals(""))
relayState = relayState + '?' + qs;
}
this.logDebug("Generating a Relay State of: " + relayState);
if (!skipURIEncoding) {
relayState = encodeURIComponent(relayState)
}
return relayState;
},
/*
* Determine if we need to bust frame and add "nav_to.do" in front of the user request URI.
*/
needNavFrame : function(request) {
var requestURI = request.getRequestURI();
// request already loading NavFrame.
if(requestURI.startsWith("/nav_to.do"))
return false;
// this request should only come from top window.
if(requestURI.startsWith("/navpage.do"))
return false;
// already bustin frames.
if(requestURI.startsWith("/saml_redirector.do"))
return false;
if(this.isCMSRequest(request))
return false;
if(requestURI.equals("") || requestURI.equals("/"))
return false;
return true;
},
isCMSRequest : function(request) {
var cmsSiteName = request.getSiteName();
this.logDebug("CMS site name: " + cmsSiteName);
if(cmsSiteName && !cmsSiteName.equals(""))
return true;
return false;
},
/*
* Create AuthnRequestObject with attributes based on options
* samlOptions = {
* providerName: sets ProviderName attribute
* forceAuthn: sets ForceAuthn attribute
* isPassive: sets IsPassive attribute
* assertionConsumerServiceURL: sets AssertionConsumerServiceURL attribute
* }
*
* If samlOptions is null, default value will be retrieved from sys_properties.
*/
createAuthnRequestWithOptions : function(samlOptions) {
var DateTime = Packages.org.joda.time.DateTime;
var SAMLVersion = Packages.org.opensaml.common.SAMLVersion;
var AuthnRequestBuilder = Packages.org.opensaml.saml2.core.impl.AuthnRequestBuilder;
var AuthnRequestMarshaller = Packages.org.opensaml.saml2.core.impl.AuthnRequestMarshaller;
var Boolean = Packages.java.lang.Boolean;
// Retrieve Default value
var serviceURL = gs.getProperty("glide.authenticate.sso.saml2.service_url");
var providerName = serviceURL;
var forceAuthn = this.isTrue(gs.getProperty("glide.authenticate.sso.saml2.defaults.force_authn", false));
var isPassive = this.isTrue(gs.getProperty("glide.authenticate.sso.saml2.defaults.is_passive", false));
var assertionConsumerServiceURL = serviceURL
var protocolBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
if (samlOptions && samlOptions.providerName)
providerName = samlOptions.providerName;
if (samlOptions && samlOptions.forceAuthn)
forceAuthn = new Boolean(samlOptions.forceAuthn);
if (samlOptions && samlOptions.isPassive)
isPassive = new Boolean(samlOptions.isPassive);
if (samlOptions && samlOptions.assertionConsumerServiceURL)
assertionConsumerServiceURL = samlOptions.assertionConsumerServiceURL;
//for later validation
gs.getSession().getHttpSession().setAttribute("glide.saml2.assertion_consumer_service_url", assertionConsumerServiceURL);
var builder = new AuthnRequestBuilder();
var authnRequest = builder.buildObject();
authnRequest.setProviderName(providerName);
authnRequest.setID(this.generateRequestID());
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setIssueInstant(new DateTime());
authnRequest.setForceAuthn(forceAuthn);
authnRequest.setIsPassive(isPassive);
authnRequest.setAssertionConsumerServiceURL(assertionConsumerServiceURL);
authnRequest.setProtocolBinding(protocolBinding);
authnRequest.setIssuer(this.createIssuer());
authnRequest.setNameIDPolicy(this.createNameIDPolicy());
authnRequest.setDestination(gs.getProperty("glide.authenticate.sso.saml2.idp_authnrequest_url"));
var createAuthnContextClassRef = this.isTrue(gs.getProperty(
"glide.authenticate.sso.saml2.createrequestedauthncontext", true));
if (createAuthnContextClassRef) {
authnRequest.setRequestedAuthnContext(this.createRequestedAuthnContext());
}
return authnRequest;
},
generateRequestID : function() {
var requestID = "SNC" + this.generateRandomId();
this.lastGeneratedRequestID = requestID;
return requestID;
},
getLastGeneratedRequestID : function() {
return this.lastGeneratedRequestID;
},
createAuthnContextClassRef : function() {
var AuthnContextClassRefBuilder = Packages.org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
var authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
var authnContextClassRef = authnContextClassRefBuilder.buildObject();
var authnContextClassRefPropertyValue = gs.getProperty("glide.authenticate.sso.saml2.authncontextclassref",
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
authnContextClassRef.setAuthnContextClassRef(authnContextClassRefPropertyValue);
return authnContextClassRef;
},
createRequestedAuthnContext : function() {
var RequestedAuthnContextBuilder = Packages.org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;
var AuthnContextComparisonTypeEnumeration = Packages.org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
var requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
var requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
requestedAuthnContext.getAuthnContextClassRefs().add(this.createAuthnContextClassRef());
return requestedAuthnContext;
},
createNameIDPolicy : function() {
var NameIDPolicyBuilder = Packages.org.opensaml.saml2.core.impl.NameIDPolicyBuilder;
var nameIdPolicyStr = gs.getProperty("glide.authenticate.sso.saml2.nameid_policy");
var serviceURLStr = gs.getProperty("glide.authenticate.sso.saml2.service_url");
if (nameIdPolicyStr == null || nameIdPolicyStr == "") {
this.logDebug("No name ID policy configured, skipping optional specification");
return null;
}
// Create NameIDPolicy
var nameIdPolicyBuilder = new NameIDPolicyBuilder();
var nameIdPolicy = nameIdPolicyBuilder.buildObject();
// insist on the emailAddress format to match with our user's email address
nameIdPolicy.setFormat(nameIdPolicyStr);
nameIdPolicy.setAllowCreate(true);
return nameIdPolicy;
},
createIssuer : function() {
var IssuerBuilder = Packages.org.opensaml.saml2.core.impl.IssuerBuilder;
var issuerStr = gs.getProperty("glide.authenticate.sso.saml2.issuer");
var issuerBuilder = new IssuerBuilder();
var issuer = issuerBuilder.buildObject();
issuer.setValue(issuerStr);
return issuer;
},
/*
* Sign a signable saml object
*/
signSAMLObject : function(samlobject) {
var Configuration = Packages.org.opensaml.Configuration;
var Signer = Packages.org.opensaml.xml.signature.Signer;
var credential = this.generateCredential();
var signature = this.generateSignature(credential);
samlobject.setSignature(signature);
var marshaller = Configuration.getMarshallerFactory().getMarshaller(samlobject);
marshaller.marshall(samlobject);
Signer.signObject(signature);
},
/*
* Generate Signature object from the SP credentails stored in sys_certificate
*/
generateSignature : function(credential) {
if(!credential)
throw new SAML2Error("Failed generating signature: credential is null");
var SignatureBuilder = Packages.org.opensaml.xml.signature.impl.SignatureBuilder;
var SignatureConstants = Packages.org.opensaml.xml.signature.SignatureConstants;
var keyInfo = this.generateKeyInfo();
var signatureBuilder = new SignatureBuilder();
var signature = signatureBuilder.buildObject();
signature.setSigningCredential(credential);
signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA);
signature.setKeyInfo(keyInfo);
this.logDebug("Signature generated for SAML request.");
return signature;
},
/*
* Generate KeyInfo object from SP credentials stored in sys_certificate
*/
generateKeyInfo : function() {
var X509KeyInfoGeneratorFactory = Packages.org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory;
var credential = this.generateCredential();
var factory = new X509KeyInfoGeneratorFactory();
factory.setEmitEntityCertificate(true);
var keyInfoGenerator = factory.newInstance();
this.logDebug("Credential: "+ credential);
var keyInfo = keyInfoGenerator.generate(credential);
this.logDebug("KeyInfo: "+keyInfo);
return keyInfo;
},
/*
* Retrieve SP private credentials from sys_certificate with alias and password
* provided in the sys_properties.
*
* Returns BasicX509Credential
*/
generateCredential : function() {
var DBKeyStoreFactory = GlideDBKeyStoreFactory;
var BasicX509Credential = Packages.org.opensaml.xml.security.x509.BasicX509Credential;
var Configuration = Packages.org.opensaml.Configuration;
var JavaString = Packages.java.lang.String;
var alias = gs.getProperty("glide.authenticate.sso.saml2.signing_key_alias");
var pw = gs.getProperty("glide.authenticate.sso.saml2.signing_key_password");
if(!alias)
throw new SAML2Error("generateCredential: sp key alias is null.");
if(!pw)
throw new SAML2Error("generateCredential: sp key password is null.");
var ksFactory = new DBKeyStoreFactory();
var ks = ksFactory.createKeyStore();
var jpw = new JavaString(pw);
var credential = new BasicX509Credential();
credential.setEntityCertificate(ks.getCertificate(alias));
credential.setPrivateKey(ks.getKey(alias, jpw.toCharArray()));
return credential;
},
/*
* construct KeyInfo from sys_certificate and returns KeyInfo xml string.
*/
generateKeyInfoXML : function() {
var XMLUtil = GlideXMLUtil;
var KeyInfoMarshaller = Packages.org.opensaml.xml.signature.impl.KeyInfoMarshaller;
var km = new KeyInfoMarshaller();
var keyInfo = this.generateKeyInfo();
var elem = km.marshall(keyInfo);
return XMLUtil.toFragmentString(elem.getOwnerDocument());
},
isHttpPostBinding : function(property) {
var binding = gs.getProperty(property);
if (binding && binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"))
return true;
return false;
},
logDebug : function(msg) {
if (this.debug == true || this.debug == "true") {
gs.log(msg);
}
},
logError : function(msg) {
this.logDebug("ERROR: " + msg);
gs.logError(msg, "SAML2");
},
logWarning : function(msg) {
this.logDebug("WARNING: " + msg);
gs.logWarning(msg, "SAML2");
},
generateRandomId : function() {
var id = "";
for ( var i = 0; i < 32; i++) {
id += ((Math.random() * 16 | 0).toString(16));
}
return id;
},
isTrue : function(s) {
if (s == true || s.toLowerCase() == "true") {
return true;
}
return false;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment