Skip to content

Commit e4d48cb

Browse files
authored
Merge pull request #66 from luuuis/allow-unsigned-assertions
feat: adds createUnsignedAssertion()
2 parents 482ae19 + 9892b16 commit e4d48cb

File tree

12 files changed

+1335
-1092
lines changed

12 files changed

+1335
-1092
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
2-
.DS_Store
2+
.DS_Store
3+
package-lock.json

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
Create SAML assertions.
1+
# node-saml
22

3-
NOTE: currently supports SAML 1.1 tokens
3+
Create SAML assertions. Supports SAML 1.1 and SAML 2.0 tokens.
44

55
[![Build Status](https://travis-ci.org/auth0/node-saml.png)](https://travis-ci.org/auth0/node-saml)
66

77
### Usage
88

99
```js
10-
var saml11 = require('saml').Saml11;
10+
var saml = require('saml').Saml20; // or Saml11
1111

1212
var options = {
1313
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
@@ -23,7 +23,7 @@ var options = {
2323
sessionIndex: '_faed468a-15a0-4668-aed6-3d9c478cc8fa'
2424
};
2525

26-
var signedAssertion = saml11.create(options);
26+
var signedAssertion = saml.create(options);
2727
```
2828

2929
Everything except the cert and key is optional.

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { extends: ['@commitlint/config-conventional'] };

lib/saml11.js

Lines changed: 113 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,109 @@
1+
var path = require('path');
2+
var utils = require('./utils');
3+
var Parser = require('xmldom').DOMParser;
4+
var xmlenc = require('xml-encryption');
5+
var moment = require('moment');
6+
var async = require('async');
7+
var crypto = require('crypto');
18

2-
var utils = require('./utils'),
3-
Parser = require('xmldom').DOMParser,
4-
SignedXml = require('xml-crypto').SignedXml,
5-
xmlenc = require('xml-encryption'),
6-
moment = require('moment');
7-
async = require('async');
8-
crypto = require('crypto');
9+
var EncryptXml = require('./xml/encrypt');
10+
var SignXml = require('./xml/sign');
911

10-
var fs = require('fs');
11-
var path = require('path');
12-
var saml11 = fs.readFileSync(path.join(__dirname, 'saml11.template')).toString();
12+
var newSaml11Document = utils.factoryForNode(path.join(__dirname, 'saml11.template'));
1313

1414
var NAMESPACE = 'urn:oasis:names:tc:SAML:1.0:assertion';
1515

16-
var algorithms = {
17-
signature: {
18-
'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
19-
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
20-
},
21-
digest: {
22-
'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256',
23-
'sha1': 'http://www.w3.org/2000/09/xmldsig#sha1'
24-
}
25-
};
16+
function extractSaml11Options(opts) {
17+
return {
18+
uid: opts.uid,
19+
issuer: opts.issuer,
20+
lifetimeInSeconds: opts.lifetimeInSeconds,
21+
audiences: opts.audiences,
22+
attributes: opts.attributes,
23+
nameIdentifier: opts.nameIdentifier,
24+
nameIdentifierFormat: opts.nameIdentifierFormat,
25+
subjectConfirmationMethod: opts.subjectConfirmationMethod,
26+
holderOfKeyProofSecret: opts.holderOfKeyProofSecret
27+
};
28+
}
2629

30+
/**
31+
* Creates a signed SAML 1.1 assertion from the given options.
32+
*
33+
* @param options
34+
*
35+
* // SAML
36+
* @param [options.uid] {string}
37+
* @param [options.issuer] {string}
38+
* @param [options.lifetimeInSeconds] {number}
39+
* @param [options.audiences] {string|string[]}
40+
* @param [options.attributes]
41+
* @param [options.nameIdentifier] {string}
42+
* @param [options.nameIdentifierFormat] {string}
43+
*
44+
* // XML Dsig
45+
* @param options.key {Buffer}
46+
* @param options.cert {Buffer}
47+
* @param [options.signatureAlgorithm] {string}
48+
* @param [options.digestAlgorithm] {string}
49+
* @param [options.signatureNamespacePrefix] {string}
50+
* @param [options.xpathToNodeBeforeSignature] {string}
51+
* @param [options.subjectConfirmationMethod] {string}
52+
* @param [options.holderOfKeyProofSecret] {Buffer}
53+
*
54+
* // XML encryption
55+
* @param [options.encryptionCert] {Buffer}
56+
* @param [options.encryptionPublicKey] {Buffer}
57+
* @param [options.encryptionAlgorithm] {string}
58+
* @param [options.keyEncryptionAlgorighm] {string}
59+
*
60+
* @param {Function} [callback] required if encrypting
61+
* @return {String|*}
62+
*/
2763
exports.create = function(options, callback) {
28-
if (!options.key)
29-
throw new Error('Expect a private key in pem format');
30-
31-
if (!options.cert)
32-
throw new Error('Expect a public key cert in pem format');
33-
34-
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
35-
options.digestAlgorithm = options.digestAlgorithm || 'sha256';
36-
37-
var cert = utils.pemToCert(options.cert);
38-
39-
var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'AssertionID' });
40-
sig.addReference("//*[local-name(.)='Assertion']",
41-
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
42-
algorithms.digest[options.digestAlgorithm]);
64+
return createAssertion(extractSaml11Options(options), {
65+
signXml: SignXml.fromSignXmlOptions(Object.assign({
66+
xpathToNodeBeforeSignature: "//*[local-name(.)='AuthenticationStatement']",
67+
signatureIdAttribute: 'AssertionID'
68+
}, options)),
69+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
70+
}, callback);
71+
}
4372

44-
sig.signingKey = options.key;
45-
46-
sig.keyInfoProvider = {
47-
getKeyInfo: function () {
48-
return "<X509Data><X509Certificate>" + cert + "</X509Certificate></X509Data>";
49-
}
50-
};
73+
/**
74+
* Creates an **unsigned** SAML 1.1 assertion from the given options.
75+
*
76+
* @param options
77+
*
78+
* // SAML
79+
* @param [options.uid] {string}
80+
* @param [options.issuer] {string}
81+
* @param [options.lifetimeInSeconds] {number}
82+
* @param [options.audiences] {string|string[]}
83+
* @param [options.attributes]
84+
* @param [options.nameIdentifier] {string}
85+
* @param [options.nameIdentifierFormat] {string}
86+
*
87+
* // XML encryption
88+
* @param [options.encryptionCert] {Buffer}
89+
* @param [options.encryptionPublicKey] {Buffer}
90+
* @param [options.encryptionAlgorithm] {string}
91+
* @param [options.keyEncryptionAlgorighm] {string}
92+
*
93+
* @param {Function} [callback] required if encrypting
94+
* @return {String|*}
95+
*/
96+
exports.createUnsignedAssertion = function(options, callback) {
97+
return createAssertion(extractSaml11Options(options), {
98+
signXml: SignXml.unsigned,
99+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
100+
}, callback);
101+
}
51102

103+
function createAssertion(options, strategies, callback) {
52104
var doc;
53105
try {
54-
doc = new Parser().parseFromString(saml11.toString());
106+
doc = newSaml11Document();
55107
} catch(err){
56108
return utils.reportError(err, callback);
57109
}
@@ -125,42 +177,38 @@ exports.create = function(options, callback) {
125177
nameIDs[1].setAttribute('Format', options.nameIdentifierFormat);
126178
}
127179

128-
if (!options.encryptionCert) return sign(options, sig, doc, callback);
180+
if (strategies.encryptXml === EncryptXml.unencrypted) {
181+
var signed = strategies.signXml(doc);
182+
return strategies.encryptXml(signed, callback);
183+
}
129184

130-
// encryption is turned on,
185+
// encryption is turned on,
131186
var proofSecret;
132187
async.waterfall([
133-
function(cb) {
134-
if (!options.subjectConfirmationMethod && options.subjectConfirmationMethod !== 'holder-of-key')
188+
function (cb) {
189+
if (!options.subjectConfirmationMethod && options.subjectConfirmationMethod !== 'holder-of-key')
135190
return cb();
136-
191+
137192
crypto.randomBytes(32, function(err, randomBytes) {
138193
proofSecret = randomBytes;
139-
addSubjectConfirmation(options, doc, options.holderOfKeyProofSecret || randomBytes, cb);
194+
addSubjectConfirmation(strategies.encryptXml.encryptOptions, doc, options.holderOfKeyProofSecret || randomBytes, cb);
140195
});
141-
142196
},
143197
function(cb) {
144-
sign(options, sig, doc, function(err, signed) {
145-
if (err) return cb(err);
146-
return encrypt(options, signed, cb);
147-
});
198+
strategies.signXml(doc, cb);
199+
},
200+
function(signed, cb) {
201+
strategies.encryptXml(signed, cb);
148202
}
149-
], function(err, result) {
203+
], function (err, result) {
150204
if (err) return callback(err);
151205
callback(null, result, proofSecret);
152206
});
153-
};
154-
155-
function addSubjectConfirmation(options, doc, randomBytes, callback) {
156-
var encryptOptions = {
157-
rsa_pub: options.encryptionPublicKey,
158-
pem: options.encryptionCert,
159-
keyEncryptionAlgorighm: options.keyEncryptionAlgorighm || 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'
160-
};
207+
}
161208

162-
xmlenc.encryptKeyInfo(randomBytes, encryptOptions, function(err, keyinfo) {
163-
if (err) return cb(err);
209+
function addSubjectConfirmation(encryptOptions, doc, randomBytes, callback) {
210+
xmlenc.encryptKeyInfo(randomBytes, encryptOptions, function(err, keyinfo) {
211+
if (err) return callback(err);
164212
var subjectConfirmationNodes = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'SubjectConfirmation');
165213

166214
for (var i=0; i<subjectConfirmationNodes.length; i++) {
@@ -179,40 +227,3 @@ function addSubjectConfirmation(options, doc, randomBytes, callback) {
179227
callback();
180228
});
181229
}
182-
183-
function sign(options, sig, doc, callback) {
184-
var token = utils.removeWhitespace(doc.toString());
185-
var signed;
186-
187-
try {
188-
var opts = options.xpathToNodeBeforeSignature ? {
189-
location: {
190-
reference: options.xpathToNodeBeforeSignature,
191-
action: 'after'
192-
}
193-
} : {};
194-
195-
sig.computeSignature(token, opts);
196-
signed = sig.getSignedXml();
197-
} catch(err){
198-
return utils.reportError(err, callback);
199-
}
200-
201-
if (!callback) return signed;
202-
203-
return callback(null, signed);
204-
}
205-
206-
function encrypt(options, signed, callback) {
207-
var encryptOptions = {
208-
rsa_pub: options.encryptionPublicKey,
209-
pem: options.encryptionCert,
210-
encryptionAlgorithm: options.encryptionAlgorithm || 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
211-
keyEncryptionAlgorighm: options.keyEncryptionAlgorighm || 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'
212-
};
213-
214-
xmlenc.encrypt(signed, encryptOptions, function(err, encrypted) {
215-
if (err) return callback(err);
216-
callback(null, utils.removeWhitespace(encrypted));
217-
});
218-
}

0 commit comments

Comments
 (0)