diff --git a/README.md b/README.md index 18130f0e..8983403e 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ type Profile = { * `privateCert`: see [Security and signatures](#security-and-signatures) * `decryptionPvk`: optional private key that will be used to attempt to decrypt any encrypted assertions that are received * `signatureAlgorithm`: optionally set the signature algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512' + * `digestAlgorithm`: optionally set the digest algorithm used to provide a digest for the signed data object, valid values are 'sha1' (default), 'sha256', or 'sha512' + * `xmlSignatureTransforms`: optionally set an array of signature transforms to be used in HTTP-POST signatures. By default this is `[ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ]` * **Additional SAML behaviors** * `additionalParams`: dictionary of additional query params to add to all requests; if an object with this key is passed to `authenticate`, the dictionary of additional query params will be appended to those present on the returned URL, overriding any specified by initialization options' additional parameters (`additionalParams`, `additionalAuthorizeParams`, and `additionalLogoutParams`) * `additionalAuthorizeParams`: dictionary of additional query params to add to 'authorize' requests diff --git a/lib/passport-saml/algorithms.js b/lib/passport-saml/algorithms.js new file mode 100644 index 00000000..cfb19d0a --- /dev/null +++ b/lib/passport-saml/algorithms.js @@ -0,0 +1,34 @@ +var crypto = require('crypto'); + +exports.getSigningAlgorithm = function getSigningAlgorithm (shortName) { + switch(shortName) { + 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: + return 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + } +}; + +exports.getDigestAlgorithm = function getDigestAlgorithm (shortName) { + switch(shortName) { + case 'sha256': + return 'http://www.w3.org/2001/04/xmlenc#sha256'; + case 'sha512': + return 'http://www.w3.org/2001/04/xmlenc#sha512'; + default: + return 'http://www.w3.org/2000/09/xmldsig#sha1'; + } +}; + +exports.getSigner = function getSigner (shortName) { + switch(shortName) { + case 'sha256': + return crypto.createSign('RSA-SHA256'); + case 'sha512': + return crypto.createSign('RSA-SHA512'); + default: + return crypto.createSign('RSA-SHA1'); + } +}; \ No newline at end of file diff --git a/lib/passport-saml/saml-post-signing.js b/lib/passport-saml/saml-post-signing.js new file mode 100644 index 00000000..58e0f3c1 --- /dev/null +++ b/lib/passport-saml/saml-post-signing.js @@ -0,0 +1,29 @@ +var SignedXml = require('xml-crypto').SignedXml; +var algorithms = require('./algorithms'); + +var authnRequestXPath = '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]'; +var issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]'; +var defaultTransforms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ]; + +function signSamlPost(samlMessage, xpath, options) { + if (!samlMessage) throw new Error('samlMessage is required'); + if (!xpath) throw new Error('xpath is required'); + if (!options || !options.privateCert) throw new Error('options.privateCert is required'); + + var transforms = options.xmlSignatureTransforms || defaultTransforms; + var sig = new SignedXml(); + if (options.signatureAlgorithm) { + sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm); + } + sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm)); + sig.signingKey = options.privateCert; + sig.computeSignature(samlMessage, { location: { reference: xpath + issuerXPath, action: 'after' }}); + return sig.getSignedXml(); +} + +function signAuthnRequestPost(authnRequest, options) { + return signSamlPost(authnRequest, authnRequestXPath, options); +} + +exports.signSamlPost = signSamlPost; +exports.signAuthnRequestPost = signAuthnRequestPost; diff --git a/lib/passport-saml/saml.js b/lib/passport-saml/saml.js index 4adcbc7e..820e9fda 100644 --- a/lib/passport-saml/saml.js +++ b/lib/passport-saml/saml.js @@ -10,6 +10,8 @@ var xmlbuilder = require('xmlbuilder'); var xmlenc = require('xml-encryption'); var xpath = xmlCrypto.xpath; var InMemoryCacheProvider = require('./inmemory-cache-provider.js').CacheProvider; +var algorithms = require('./algorithms'); +var signAuthnRequestPost = require('./saml-post-signing').signAuthnRequestPost; var Q = require('q'); var SAML = function (options) { @@ -122,20 +124,8 @@ SAML.prototype.generateInstant = function () { SAML.prototype.signRequest = function (samlMessage) { var signer; var samlMessageToSign = {}; - switch(this.options.signatureAlgorithm) { - case 'sha256': - samlMessage.SigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; - signer = crypto.createSign('RSA-SHA256'); - break; - case 'sha512': - samlMessage.SigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; - signer = crypto.createSign('RSA-SHA512'); - break; - default: - samlMessage.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; - signer = crypto.createSign('RSA-SHA1'); - break; - } + samlMessage.SigAlg = algorithms.getSigningAlgorithm(this.options.signatureAlgorithm); + signer = algorithms.getSigner(this.options.signatureAlgorithm); if (samlMessage.SAMLRequest) { samlMessageToSign.SAMLRequest = samlMessage.SAMLRequest; } @@ -152,7 +142,7 @@ SAML.prototype.signRequest = function (samlMessage) { samlMessage.Signature = signer.sign(this.options.privateCert, 'base64'); }; -SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) { +SAML.prototype.generateAuthorizeRequest = function (req, isPassive, isHttpPostBinding, callback) { var id = "_" + this.generateUniqueID(); var instant = this.generateInstant(); var forceAuthn = this.options.forceAuthn || false; @@ -223,7 +213,11 @@ SAML.prototype.generateAuthorizeRequest = function (req, isPassive, callback) { request['samlp:AuthnRequest']['@ProviderName'] = this.options.providerName; } - callback(null, xmlbuilder.create(request).end()); + var stringRequest = xmlbuilder.create(request).end(); + if (isHttpPostBinding && this.options.privateCert) { + stringRequest = signAuthnRequestPost(stringRequest, this.options); + } + callback(null, stringRequest); }) .fail(function(err){ callback(err); @@ -303,7 +297,7 @@ SAML.prototype.generateLogoutResponse = function (req, logoutRequest) { }; SAML.prototype.requestToUrl = function (request, response, operation, additionalParameters, callback) { - + const requestToUrlHelper = (err, buffer) => { if (err) { return callback(err); @@ -395,7 +389,7 @@ SAML.prototype.getAdditionalParams = function (req, operation, overrideParams) { }; SAML.prototype.getAuthorizeUrl = function (req, options, callback) { - this.generateAuthorizeRequest(req, this.options.passive, (err, request) => { + this.generateAuthorizeRequest(req, this.options.passive, false, (err, request) => { if (err) return callback(err); var operation = 'authorize'; @@ -462,7 +456,7 @@ SAML.prototype.getAuthorizeForm = function (req, callback) { ].join('\r\n')); }; - this.generateAuthorizeRequest(req, this.options.passive, (err, request) => { + this.generateAuthorizeRequest(req, this.options.passive, true, (err, request) => { if (err) { return callback(err); } @@ -537,7 +531,7 @@ SAML.prototype.validateSignature = function (fullXml, currentNode, certs) { if (signatures.length != 1) { return false; } - + const signature = signatures[0]; return certs.some(certToCheck => { return this.validateSignatureForCert(signature, certToCheck, fullXml, currentNode); @@ -904,7 +898,7 @@ SAML.prototype.processValidlySignedAssertion = function(xml, samlResponseXml, in if (inResponseTo) { profile.inResponseTo = inResponseTo; } - + var authnStatement = assertion.AuthnStatement; if (authnStatement) { if (authnStatement[0].$ && authnStatement[0].$.SessionIndex) { diff --git a/test/saml-post-signing-tests.js b/test/saml-post-signing-tests.js new file mode 100644 index 00000000..d9fecb9c --- /dev/null +++ b/test/saml-post-signing-tests.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const should = require('should'); +const samlPostSigning = require('../lib/passport-saml/saml-post-signing'); +const signSamlPost = samlPostSigning.signSamlPost; +const signAuthnRequestPost = samlPostSigning.signAuthnRequestPost; + +const signingKey = fs.readFileSync(__dirname + '/static/key.pem'); + +describe('SAML POST Signing', function () { + it('should sign a simple saml request', function () { + var xml = 'http://example.com'; + var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); + + it('should place the Signature element after the Issuer element', function () { + var xml = 'http://example.com'; + var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey }); + result.should.match(/<\/saml2:Issuer>/); + result.should.match(//); + result.should.match(//); + }); + + it('should sign an AuthnRequest', function () { + var xml = 'http://example.com'; + var result = signAuthnRequestPost(xml, { privateCert: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); +});