Created
April 8, 2014 13:25
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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