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>/);
+ });
+});