Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #206: Support signing AuthnRequests using the HTTP-POST Binding #207

Merged
merged 3 commits into from
Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions lib/passport-saml/algorithms.js
Original file line number Diff line number Diff line change
@@ -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');
}
};
29 changes: 29 additions & 0 deletions lib/passport-saml/saml-post-signing.js
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 15 additions & 21 deletions lib/passport-saml/saml.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 44 additions & 0 deletions test/saml-post-signing-tests.js
Original file line number Diff line number Diff line change
@@ -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 = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></SAMLRequest>';
var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey });
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should place the Signature element after the Issuer element', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer><SomeOtherElement /></SAMLRequest>';
var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey });
result.should.match(/<\/saml2:Issuer><Signature/);
result.should.match(/<\/Signature><SomeOtherElement/);
});

it('should sign and digest with SHA256 when specified', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></SAMLRequest>';
var options = {
signatureAlgorithm: 'sha256',
digestAlgorithm: 'sha256',
privateCert: signingKey
}
var result = signSamlPost(xml, '/SAMLRequest', options);
result.should.match(/<SignatureMethod Algorithm="http:\/\/www.w3.org\/2001\/04\/xmldsig-more#rsa-sha256"/);
result.should.match(/<Transform Algorithm="http:\/\/www.w3.org\/2001\/10\/xml-exc-c14n#"\/>/);
result.should.match(/<Transform Algorithm="http:\/\/www.w3.org\/2000\/09\/xmldsig#enveloped-signature"\/>/);
result.should.match(/<DigestMethod Algorithm="http:\/\/www.w3.org\/2001\/04\/xmlenc#sha256"\/>/);
});

it('should sign an AuthnRequest', function () {
var xml = '<AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:protocol"><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></AuthnRequest>';
var result = signAuthnRequestPost(xml, { privateCert: signingKey });
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});
});