Skip to content

Commit

Permalink
Allow for use of privateKey instead of privateCert (#488)
Browse files Browse the repository at this point in the history
* Allow for use of privateKey instead of privateCert

Co-authored-by: Mark Stosberg <[email protected]>
  • Loading branch information
alon85 and markstos authored Nov 3, 2020
1 parent 0f1a414 commit 8046db0
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 16 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ type Profile = {
* `issuer`: issuer string to supply to identity provider
* `audience`: expected saml response Audience (if not provided, Audience won't be verified)
* `cert`: the IDP's public signing certificate used to validate the signatures of the incoming SAML Responses, see [Security and signatures](#security-and-signatures)
* `privateCert`: see [Security and signatures](#security-and-signatures)
* `privateKey`: see [Security and signatures](#security-and-signatures). Old name of `privateCert` is accepted alternative.
* `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'
Expand Down Expand Up @@ -238,9 +238,9 @@ To select hashing algorithm, use:
...
```
To sign them you need to provide a private key in the PEM format via the `privateCert` configuration key.
To sign them you need to provide a private key in the PEM format via the `privateKey` configuration key.
Formats supported for `privateCert` field are,
Formats supported for `privateKey` field are,
1. Well formatted PEM:
Expand Down
7 changes: 4 additions & 3 deletions src/passport-saml/saml-post-signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:n
const defaultTransforms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ];

interface SignSamlPostOptions {
privateCert: string;
privateCert?: string;
privateKey?: string;
signatureAlgorithm?: string;
xmlSignatureTransforms?: string[];
digestAlgorithm: string;
Expand All @@ -15,15 +16,15 @@ interface SignSamlPostOptions {
export function signSamlPost(samlMessage: string, xpath: string, options: SignSamlPostOptions) {
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');
if (!options || (!options.privateCert && !options.privateKey)) throw new Error('options.privateCert or options.privateKey is required');

const transforms = options.xmlSignatureTransforms || defaultTransforms;
const 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.signingKey = options.privateCert || options.privateKey;
sig.computeSignature(samlMessage, { location: { reference: xpath + issuerXPath, action: 'after' }});
return sig.getSignedXml();
}
Expand Down
18 changes: 10 additions & 8 deletions src/passport-saml/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ interface SAMLOptions {
signatureAlgorithm: string;
path: string;
privateCert: string;
privateKey: string;
logoutUrl: string;
entryPoint: string;
skipRequestCompression: boolean;
Expand Down Expand Up @@ -232,7 +233,7 @@ class SAML {
samlMessageToSign.SigAlg = samlMessage.SigAlg;
}
signer.update(querystring.stringify(samlMessageToSign));
samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert), 'base64');
samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert) || this.options.privateKey, 'base64');
}

generateAuthorizeRequest = function (req, isPassive, isHttpPostBinding, callback) {
Expand Down Expand Up @@ -357,7 +358,8 @@ class SAML {
}

let stringRequest = xmlbuilder.create(request).end();
if (isHttpPostBinding && this.options.privateCert) {
const privateKey = this.options.privateCert || this.options.privateKey;
if (isHttpPostBinding && privateKey) {
stringRequest = signAuthnRequestPost(stringRequest, this.options);
}
callback(null, stringRequest);
Expand Down Expand Up @@ -464,8 +466,8 @@ class SAML {
Object.keys(additionalParameters).forEach(k => {
samlMessage[k] = additionalParameters[k];
});

if (this.options.privateCert) {
const privateKey = this.options.privateCert || this.options.privateKey;
if (privateKey) {
try {
if (!this.options.entryPoint) {
throw new Error('"entryPoint" config parameter is required for signed messages');
Expand Down Expand Up @@ -1303,17 +1305,17 @@ class SAML {
"Missing decryptionCert while generating metadata for decrypting service provider");
}
}

if(this.options.privateCert){
const privateKey = this.options.privateCert || this.options.privateKey;
if(privateKey){
if(!signingCert){
throw new Error(
"Missing signingCert while generating metadata for signing service provider messages");
}
}

if(this.options.decryptionPvk || this.options.privateCert){
if(this.options.decryptionPvk || privateKey){
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor=[];
if (this.options.privateCert) {
if (privateKey) {

signingCert = signingCert.replace( /-+BEGIN CERTIFICATE-+\r?\n?/, '' );
signingCert = signingCert.replace( /-+END CERTIFICATE-+\r?\n?/, '' );
Expand Down
38 changes: 37 additions & 1 deletion test/saml-post-signing-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,48 @@ describe('SAML POST Signing', function () {
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should sign a simple saml request when using a privateKey', 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', { privateKey: 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 place the Signature element after the Issuer element when using a privateKey', 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', { privateKey: 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 and digest with SHA256 when specified and using privateKey', 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',
privateKey: 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#"\/>/);
Expand All @@ -41,4 +69,12 @@ describe('SAML POST Signing', function () {
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should sign an AuthnRequest when using a privateKey', 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, { privateKey: signingKey });
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

});
137 changes: 136 additions & 1 deletion test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,22 @@ describe( 'passport-saml /', function() {
testMetadata( samlConfig, expectedMetadata, signingCert );
});

it( 'config with protocol, path, host, decryptionPvk and privateKey should pass', function() {
var samlConfig = {
issuer: 'http://example.serviceprovider.com',
protocol: 'http://',
host: 'example.serviceprovider.com',
path: '/saml/callback',
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
decryptionPvk: fs.readFileSync(__dirname + '/static/testshib encryption pvk.pem'),
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key')
};
var expectedMetadata = fs.readFileSync(__dirname + '/static/expectedMetadataWithBothKeys.xml', 'utf-8');
var signingCert = fs.readFileSync(__dirname + '/static/acme_tools_com.cert').toString();

testMetadata( samlConfig, expectedMetadata, signingCert );
});

});

it('generateServiceProviderMetadata contains logout callback url', function () {
Expand Down Expand Up @@ -1895,7 +1911,33 @@ describe( 'passport-saml /', function() {
}
});
});

it( 'acme_tools request signed with sha256 when using privateKey', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
identifierFormat: null,
signatureAlgorithm: 'sha256',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };
samlObj.getAuthorizeUrl({}, {}, function(err, url) {
try {
var qry = require('querystring').parse(require('url').parse(url).query);
qry.SigAlg.should.match('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256');
qry.Signature.should.match('hel9NaoLU0brY/VhrQsY+lTtuAbTsT/ul6nZ/eVlSMXQRaKn5LTbKadzxmPghX7s4xoHwdah+yZHK/0u4StYSj4b5MKcqbsJapVr2R7H90z8YfGfR2C/G0Gng721YV9Da6VBzKg8Was91zQotgsMpZ9pGX1kPKi6cgFwPwM4NEFugn8AYgXEriNvO5+Q23K/MdBT2bgwRTj2FQCWTuQcgwbyWHXoquHztZ0lbh8UhY5BfQRv7c6D9XPkQEMMQFQeME4PIEg3JnynwFZk5wwhkphMd5nXxau+zt7Nfp4fRm0G8WYnxV1etBnWimwSglZVaSHFYeQBRsC2wvKSiVS8JA==');
qry.customQueryStringParam.should.match('CustomQueryStringParamValue');
done();
} catch (err2) {
done(err2);
}
});
});
it( 'acme_tools request not signed if missing entry point', function( done ) {
var samlConfig = {
entryPoint: '',
Expand All @@ -1922,7 +1964,32 @@ describe( 'passport-saml /', function() {
}
});
});
it( 'acme_tools request not signed if missing entry point when using privateKey', function( done ) {
var samlConfig = {
entryPoint: '',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
signatureAlgorithm: 'sha256',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };

var request = '<?xml version=\\"1.0\\"?><samlp:AuthnRequest xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" ID=\\"_ea40a8ab177df048d645\\" Version=\\"2.0\\" IssueInstant=\\"2017-08-22T19:30:01.363Z\\" ProtocolBinding=\\"urn:oasis:names$tc:SAML:2.0:bindings:HTTP-POST\\" AssertionConsumerServiceURL=\\"https://example.com/login/callback\\" Destination=\\"https://www.example.com\\"><saml:Issuer xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">onelogin_saml</saml:Issuer><s$mlp:NameIDPolicy xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" Format=\\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\\" AllowCreate=\\"true\\"/><samlp:RequestedAuthnContext xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protoc$l\\" Comparison=\\"exact\\"><saml:AuthnContextClassRef xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp$AuthnRequest>';
samlObj.requestToUrl(request, null, 'authorize', {}, function(err) {
try {
should.exist(err);
err.message.should.eql('"entryPoint" config parameter is required for signed messages');
done();
} catch (err2) {
done(err2);
}
});
});
it( 'acme_tools request signed with sha1', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
Expand Down Expand Up @@ -1950,6 +2017,33 @@ describe( 'passport-saml /', function() {
}
});
});
it( 'acme_tools request signed with sha1 when using privateKey', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
identifierFormat: null,
signatureAlgorithm: 'sha1',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };
samlObj.getAuthorizeUrl({}, {}, function(err, url) {
try {
var qry = require('querystring').parse(require('url').parse(url).query);
qry.SigAlg.should.match('http://www.w3.org/2000/09/xmldsig#rsa-sha1');
qry.Signature.should.match('MeFo+LjufxP5A+sCRwzR/YH/RV6W14aYSFjUdie62JxkI6hDcVhoSZQUJ3wtWMhL59gJj05tTFnXAZRqUQVsavyy41cmUZVeCsat0gaHBQOILXpp9deB0iSJt1EVQTOJkVx8uu2/WYu/bBiH7w2bpwuCf1gJhlqZb/ca3B6yjHSMjnnVfc2LbNPWHpE5464lrs79VjDXf9GQWfrBr95dh3P51IAb7C+77KDWQUl9WfZfyyuEgS83vyZ0UGOxT4AObJ6NOcLs8+iidDdWJJkBaKQev6U+AghCjLQUYOrflivLIIyqATKu2q9PbOse6Phmnxok50+broXSG23+e+742Q==');
qry.customQueryStringParam.should.match('CustomQueryStringParamValue');
done();
} catch (err2) {
done(err2);
}
});
});
});

describe( 'getAdditionalParams checks /', function() {
Expand Down Expand Up @@ -2831,6 +2925,47 @@ describe( 'passport-saml /', function() {
}
});
});
it('errors if bad privateKey to requestToURL', function(done){
var samlObj = new SAML({
entryPoint: "foo",
privateKey: "-----BEGIN CERTIFICATE-----\n"+
"8mvhvrcCOiJ3mjgKNN1F31jOBJuZNmq0U7n9v+Z+3NfyU/0E9jkrnFvm5ks+p8kl\n" +
"BjuBk9RAkazsU9l02XMS/VxOOIifxKC7R9bDtx0hjolYxgqxPIO5s4rmjj0rLzvo\n" +
"vQTTTx/tB5e+hbdx922QSeTjP4DO4ms6cIexcH+ZEUOJ3wXiHToJW83SXLRtwPI9\n" +
"JbWKeS9nWPnzcedbDNZkGtohW5vf32BHuvLsWcl6eFXRSkdX/7+rgpXmDRB7caQ+\n" +
"2SXVY7ORily7LTKg1cFmuKHDzKTGFIp5/GU6dwIDAQABAoIBAArgFQ+Uk4UN4diY\n" +
"gJWCAaQlTVmP0UEHZQt/NmJrc9ZVduuhOP0hH6gF53nREHz5UQb4nXB2Ksa3MtYD\n" +
"Z1vhJcu/T7pvmib4q+Ij6oAmlyeL/xwVY3IUURMxX3tCdPItlk4PEFELKeqQOiIS\n" +
"7B0DYxWfJbMle3c95w5ruYEr2A+fHCKVSlDpg7uPd9VQ6t7bGMZZvc9tDSC1qPXQ\n" +
"Gd/WOMXxi+t/TpyVZ6tOcEekQzAMLmWElUUPx3TJ0ur0Zl2LZ7IvQEXXias4lUHV\n" +
"fnH3akDCMmdhlJSVqUfplrh85zAOh6fLloZagphj/Kpgfw1TZ+njSDYqSLYE0NZ1\n" +
"j+83feECgYEA2aNGgbc+t6QLrJJ63l9Mz541lVV3IUAxZ5ACqOnMkQVuLoa5IMwM\n" +
"oENIo38ptfHQqjQ9x8/tEINFqOHnQuOJ/+1xP9f0Me+0clRDCqjGYqNYgmakKyD7\n" +
"vey/q6kwHk679RVGiI1p+HdoA+CbEKWHJiRxE0RhAA3G3wGAq7kpJocCgYEAxp4/\n" +
"tCft+eHVRivspfDN//axc2TR6qWP9E1ueGvbiXPXv0Puag0W9cER/df/s5jW4Rqg\n" +
"CE8649HPUZ0FJT+YaeKgu2Sw9SMcGl4/uyHzg7KnXIeYyQZJPqQkKyXmIix8cw3+\n" +
"HBGRtwX5nOy0DgFdaMiH0F08peNI9QHKKTBoWJECgYEAyymJ1ekzWMaAR1Zt8EvS\n" +
"LjWoG4EuthFwjRZ4BSpLVk1Vb4VAKAeS+cAVfNpmG3xip6Ag0/ebe0CvtFk9QsmZ\n" +
"txj2EP0M7div/9H8y2SF3OpS41fhhIlDtyXcPuivDHu/Jaf4sdwgwlrk9EmlN0Lu\n" +
"CIMYMz4vtpclwGNss+EjMt0CgYEAqepD0Vm/iuCaVhfJsgSaFvnywSdlNfpBdtyv\n" +
"PzH2dFa4IZZ55hwgoklznNgmlnyQh68BbVpqpO+fDtDnz//h4ePRYb84a96Hcj9j\n" +
"AjJ/YxF5f/04xfEsw/wkPQ2FHYM1TDCSTWzyXcMs0gTl3H1qbfPvzF+XPMt+ZKwN\n" +
"SMNy4SECgYB3ig6t+XVfNkw8oBOh0Gx37XKbmImXsA8ucDAX9KUbMIvD03XCEf34\n" +
"jF3SNJh0SmHoT62vc+cJqPxMDP6E7Q1nZxsEyaAkKr2H4dSM4SlRm0VB+bS+jXsz\n" +
"PCiRGSm8eupuxfix05LMMreo4mC7e3Ir4JhdCsXxAMZIvbNyXcvUMA==\n" +
"-----END CERTIFICATE-----\n"
});
var request = '<?xml version=\\"1.0\\"?><samlp:AuthnRequest xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" ID=\\"_ea40a8ab177df048d645\\" Version=\\"2.0\\" IssueInstant=\\"2017-08-22T19:30:01.363Z\\" ProtocolBinding=\\"urn:oasis:names$tc:SAML:2.0:bindings:HTTP-POST\\" AssertionConsumerServiceURL=\\"https://example.com/login/callback\\" Destination=\\"https://www.example.com\\"><saml:Issuer xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">onelogin_saml</saml:Issuer><s$mlp:NameIDPolicy xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" Format=\\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\\" AllowCreate=\\"true\\"/><samlp:RequestedAuthnContext xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protoc$l\\" Comparison=\\"exact\\"><saml:AuthnContextClassRef xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp$AuthnRequest>';
samlObj.requestToUrl(request, null, 'authorize', {}, function(err) {
try {
should.exist(err);
err.message.should.containEql('no start line');
done();
} catch (err2) {
done(err2);
}
});
});
});

describe('validateRedirect()', function() {
Expand Down

0 comments on commit 8046db0

Please sign in to comment.