Skip to content

Commit 40ffebd

Browse files
authored
Merge pull request #16 from auth0/name-format-and-type
Added nameformat and type for attributes
2 parents 98e60ec + 63ed7aa commit 40ffebd

File tree

3 files changed

+261
-6
lines changed

3 files changed

+261
-6
lines changed

lib/saml20.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ var utils = require('./utils'),
33
Parser = require('xmldom').DOMParser,
44
SignedXml = require('xml-crypto').SignedXml,
55
xmlenc = require('xml-encryption'),
6-
moment = require('moment');
6+
moment = require('moment'),
7+
xmlNameValidator = require('xml-name-validator'),
8+
is_uri = require('valid-url').is_uri;
79

810
var fs = require('fs');
911
var path = require('path');
@@ -22,6 +24,37 @@ var algorithms = {
2224
}
2325
};
2426

27+
function getAttributeType(value){
28+
switch(typeof value) {
29+
case "string":
30+
return 'xs:string';
31+
case "boolean":
32+
return 'xs:boolean';
33+
case "number":
34+
// Maybe we should fine-grain this type and check whether it is an integer, float, double xsi:types
35+
return 'xs:double';
36+
default:
37+
return 'xs:anyType';
38+
}
39+
}
40+
41+
function getNameFormat(name){
42+
if (is_uri(name)){
43+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri';
44+
}
45+
46+
// Check that the name is a valid xs:Name -> https://www.w3.org/TR/xmlschema-2/#Name
47+
// xmlNameValidate.name takes a string and will return an object of the form { success, error },
48+
// where success is a boolean
49+
// if it is false, then error is a string containing some hint as to where the match went wrong.
50+
if (xmlNameValidator.name(name).success){
51+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic';
52+
}
53+
54+
// Default value
55+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified';
56+
}
57+
2558
exports.create = function(options, callback) {
2659
if (!options.key)
2760
throw new Error('Expect a private key in pem format');
@@ -32,6 +65,10 @@ exports.create = function(options, callback) {
3265
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
3366
options.digestAlgorithm = options.digestAlgorithm || 'sha256';
3467

68+
options.includeAttributeNameFormat = (typeof options.includeAttributeNameFormat !== 'undefined') ? options.includeAttributeNameFormat : true;
69+
options.typedAttributes = (typeof options.typedAttributes !== 'undefined') ? options.typedAttributes : true;
70+
71+
3572
var cert = utils.pemToCert(options.cert);
3673

3774
var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'ID' });
@@ -97,15 +134,24 @@ exports.create = function(options, callback) {
97134
// </saml:Attribute>
98135
var attributeElement = doc.createElementNS(NAMESPACE, 'saml:Attribute');
99136
attributeElement.setAttribute('Name', prop);
137+
138+
if (options.includeAttributeNameFormat){
139+
attributeElement.setAttribute('NameFormat', getNameFormat(prop));
140+
}
141+
100142
var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]];
101143
values.forEach(function (value) {
102-
var valueElement = doc.createElementNS(NAMESPACE, 'saml:AttributeValue');
103-
valueElement.setAttribute('xsi:type', 'xs:anyType');
104-
valueElement.textContent = value;
105-
attributeElement.appendChild(valueElement);
144+
// Check by type, becase we want to include false values
145+
if (typeof value !== 'undefined') {
146+
// Ignore undefined values in Array
147+
var valueElement = doc.createElementNS(NAMESPACE, 'saml:AttributeValue');
148+
valueElement.setAttribute('xsi:type', options.typedAttributes ? getAttributeType(value) : 'xs:anyType');
149+
valueElement.textContent = value;
150+
attributeElement.appendChild(valueElement);
151+
}
106152
});
107153

108-
if (values && values.length > 0) {
154+
if (values && values.filter(function(i){ return typeof i !== 'undefined'; }).length > 0) {
109155
// saml:Attribute must have at least one saml:AttributeValue
110156
statement.appendChild(attributeElement);
111157
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
"dependencies": {
1717
"async": "~0.2.9",
1818
"moment": "~2.14.1",
19+
"valid-url": "~1.0.9",
1920
"xml-crypto": "0.8.4",
2021
"xml-encryption": "~0.7.4",
22+
"xml-name-validator": "~2.0.1",
2123
"xmldom": "=0.1.15",
2224
"xpath": "0.0.5"
2325
},

test/saml20.tests.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,213 @@ describe('saml 2.0', function () {
8484
assert.equal('fóo', attributes[2].textContent);
8585
});
8686

87+
it('should set attributes with the correct attribute type', function () {
88+
var options = {
89+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
90+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
91+
attributes: {
92+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': '[email protected]',
93+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar',
94+
'http://example.org/claims/testemptyarray': [], // should dont include empty arrays
95+
'http://example.org/claims/testaccent': 'fóo', // should supports accents
96+
'http://attributes/boolean': true,
97+
'http://attributes/booleanNegative': false,
98+
'http://attributes/number': 123,
99+
'http://undefinedattribute/ws/com.com': undefined
100+
}
101+
};
102+
103+
var signedAssertion = saml.create(options);
104+
105+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
106+
assert.equal(true, isValid);
107+
108+
var attributes = utils.getAttributes(signedAssertion);
109+
assert.equal(6, attributes.length);
110+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name'));
111+
assert.equal('[email protected]', attributes[0].textContent);
112+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name'));
113+
assert.equal('Foo Bar', attributes[1].textContent);
114+
assert.equal('http://example.org/claims/testaccent', attributes[2].getAttribute('Name'));
115+
assert.equal('xs:string', attributes[2].firstChild.getAttribute('xsi:type'));
116+
assert.equal('fóo', attributes[2].textContent);
117+
assert.equal('http://attributes/boolean', attributes[3].getAttribute('Name'));
118+
assert.equal('xs:boolean', attributes[3].firstChild.getAttribute('xsi:type'));
119+
assert.equal('true', attributes[3].textContent);
120+
assert.equal('http://attributes/booleanNegative', attributes[4].getAttribute('Name'));
121+
assert.equal('xs:boolean', attributes[4].firstChild.getAttribute('xsi:type'));
122+
assert.equal('false', attributes[4].textContent);
123+
assert.equal('http://attributes/number', attributes[5].getAttribute('Name'));
124+
assert.equal('xs:double', attributes[5].firstChild.getAttribute('xsi:type'));
125+
assert.equal('123', attributes[5].textContent);
126+
});
127+
128+
it('should set attributes with the correct attribute type and NameFormat', function () {
129+
var options = {
130+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
131+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
132+
attributes: {
133+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': '[email protected]',
134+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar',
135+
'http://example.org/claims/testemptyarray': [], // should dont include empty arrays
136+
'testaccent': 'fóo', // should supports accents
137+
'urn:test:1:2:3': true,
138+
'123~oo': 123,
139+
'http://undefinedattribute/ws/com.com': undefined
140+
}
141+
};
142+
143+
var signedAssertion = saml.create(options);
144+
145+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
146+
assert.equal(true, isValid);
147+
148+
var attributes = utils.getAttributes(signedAssertion);
149+
assert.equal(5, attributes.length);
150+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name'));
151+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[0].getAttribute('NameFormat'));
152+
assert.equal('[email protected]', attributes[0].textContent);
153+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name'));
154+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[1].getAttribute('NameFormat'));
155+
assert.equal('Foo Bar', attributes[1].textContent);
156+
assert.equal('testaccent', attributes[2].getAttribute('Name'));
157+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:basic', attributes[2].getAttribute('NameFormat'));
158+
assert.equal('xs:string', attributes[2].firstChild.getAttribute('xsi:type'));
159+
assert.equal('fóo', attributes[2].textContent);
160+
assert.equal('urn:test:1:2:3', attributes[3].getAttribute('Name'));
161+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[3].getAttribute('NameFormat'));
162+
assert.equal('xs:boolean', attributes[3].firstChild.getAttribute('xsi:type'));
163+
assert.equal('true', attributes[3].textContent);
164+
assert.equal('123~oo', attributes[4].getAttribute('Name'));
165+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified', attributes[4].getAttribute('NameFormat'));
166+
assert.equal('xs:double', attributes[4].firstChild.getAttribute('xsi:type'));
167+
assert.equal('123', attributes[4].textContent);
168+
});
169+
170+
it('should set attributes to anytpe when typedAttributes is false', function () {
171+
var options = {
172+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
173+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
174+
typedAttributes: false,
175+
attributes: {
176+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': '[email protected]',
177+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar',
178+
'http://example.org/claims/testemptyarray': [], // should dont include empty arrays
179+
'http://example.org/claims/testaccent': 'fóo', // should supports accents
180+
'http://attributes/boolean': true,
181+
'http://attributes/number': 123,
182+
'http://undefinedattribute/ws/com.com': undefined
183+
}
184+
};
185+
186+
var signedAssertion = saml.create(options);
187+
188+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
189+
assert.equal(true, isValid);
190+
191+
var attributes = utils.getAttributes(signedAssertion);
192+
assert.equal(5, attributes.length);
193+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name'));
194+
assert.equal('[email protected]', attributes[0].textContent);
195+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name'));
196+
assert.equal('Foo Bar', attributes[1].textContent);
197+
assert.equal('http://example.org/claims/testaccent', attributes[2].getAttribute('Name'));
198+
assert.equal('xs:anyType', attributes[2].firstChild.getAttribute('xsi:type'));
199+
assert.equal('fóo', attributes[2].textContent);
200+
assert.equal('http://attributes/boolean', attributes[3].getAttribute('Name'));
201+
assert.equal('xs:anyType', attributes[3].firstChild.getAttribute('xsi:type'));
202+
assert.equal('true', attributes[3].textContent);
203+
assert.equal('http://attributes/number', attributes[4].getAttribute('Name'));
204+
assert.equal('xs:anyType', attributes[4].firstChild.getAttribute('xsi:type'));
205+
assert.equal('123', attributes[4].textContent);
206+
});
207+
208+
it('should not set NameFormat in attributes when includeAttributeNameFormat is false', function () {
209+
var options = {
210+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
211+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
212+
typedAttributes: false,
213+
includeAttributeNameFormat: false,
214+
attributes: {
215+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': '[email protected]',
216+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar',
217+
'http://example.org/claims/testemptyarray': [], // should dont include empty arrays
218+
'testaccent': 'fóo', // should supports accents
219+
'urn:test:1:2:3': true,
220+
'123~oo': 123,
221+
'http://undefinedattribute/ws/com.com': undefined
222+
}
223+
};
224+
225+
var signedAssertion = saml.create(options);
226+
227+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
228+
assert.equal(true, isValid);
229+
230+
var attributes = utils.getAttributes(signedAssertion);
231+
assert.equal(5, attributes.length);
232+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name'));
233+
assert.equal('', attributes[0].getAttribute('NameFormat'));
234+
assert.equal('[email protected]', attributes[0].textContent);
235+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name'));
236+
assert.equal('', attributes[1].getAttribute('NameFormat'));
237+
assert.equal('Foo Bar', attributes[1].textContent);
238+
assert.equal('testaccent', attributes[2].getAttribute('Name'));
239+
assert.equal('', attributes[2].getAttribute('NameFormat'));
240+
assert.equal('fóo', attributes[2].textContent);
241+
assert.equal('urn:test:1:2:3', attributes[3].getAttribute('Name'));
242+
assert.equal('', attributes[3].getAttribute('NameFormat'));
243+
assert.equal('true', attributes[3].textContent);
244+
assert.equal('123~oo', attributes[4].getAttribute('Name'));
245+
assert.equal('', attributes[4].getAttribute('NameFormat'));
246+
assert.equal('123', attributes[4].textContent);
247+
});
248+
249+
it('should ignore undefined attributes in array', function () {
250+
var options = {
251+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
252+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
253+
attributes: {
254+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': '[email protected]',
255+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'Foo Bar',
256+
'http://example.org/claims/testemptyarray': [], // should dont include empty arrays
257+
'arrayAttribute': [ 'foo', undefined, 'bar'],
258+
'urn:test:1:2:3': true,
259+
'123~oo': 123,
260+
'http://undefinedattribute/ws/com.com': undefined
261+
}
262+
};
263+
264+
var signedAssertion = saml.create(options);
265+
266+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
267+
assert.equal(true, isValid);
268+
269+
var attributes = utils.getAttributes(signedAssertion);
270+
assert.equal(5, attributes.length);
271+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', attributes[0].getAttribute('Name'));
272+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[0].getAttribute('NameFormat'));
273+
assert.equal('[email protected]', attributes[0].textContent);
274+
assert.equal('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', attributes[1].getAttribute('Name'));
275+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[1].getAttribute('NameFormat'));
276+
assert.equal('Foo Bar', attributes[1].textContent);
277+
assert.equal('arrayAttribute', attributes[2].getAttribute('Name'));
278+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:basic', attributes[2].getAttribute('NameFormat'));
279+
assert.equal('xs:string', attributes[2].firstChild.getAttribute('xsi:type'));
280+
assert.equal(2, attributes[2].childNodes.length);
281+
assert.equal('foo', attributes[2].childNodes[0].textContent);
282+
// undefined should not be here
283+
assert.equal('bar', attributes[2].childNodes[1].textContent);
284+
assert.equal('urn:test:1:2:3', attributes[3].getAttribute('Name'));
285+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', attributes[3].getAttribute('NameFormat'));
286+
assert.equal('xs:boolean', attributes[3].firstChild.getAttribute('xsi:type'));
287+
assert.equal('true', attributes[3].textContent);
288+
assert.equal('123~oo', attributes[4].getAttribute('Name'));
289+
assert.equal('urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified', attributes[4].getAttribute('NameFormat'));
290+
assert.equal('xs:double', attributes[4].firstChild.getAttribute('xsi:type'));
291+
assert.equal('123', attributes[4].textContent);
292+
});
293+
87294
it('whole thing with specific authnContextClassRef', function () {
88295
var options = {
89296
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),

0 commit comments

Comments
 (0)