From 9b4fdb30a3013bc5fe6232c563c0ea3a17bc6e70 Mon Sep 17 00:00:00 2001 From: athieme Date: Fri, 13 Jun 2014 15:25:56 -0400 Subject: [PATCH 1/7] chanes in support of saml assertions where xml is prefixed by 'saml' namespace identifier --- lib/saml20.js | 76 +++++++++++++++++++++++++++------------- lib/validateSignature.js | 2 +- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index c00b25a..a6899aa 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -1,43 +1,69 @@ var nameIdentifierClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'; var saml20 = module.exports; +var _ = require('lodash'); saml20.parse = function (assertion) { - var claims = {}; - if (assertion.AttributeStatement) { - var attributes = assertion.AttributeStatement.Attribute; + var claims = {}; - if (attributes) { - attributes = (attributes instanceof Array) ? attributes : [attributes]; + if (assertion.AttributeStatement + || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:AttributeStatement'])) { - attributes.forEach(function (attribute) { - claims[attribute['@'].Name] = attribute.AttributeValue; - }); - } - } + var attributes; + if (assertion.AttributeStatement && assertion.AttributeStatement.Attribute) { + attributes = assertion.AttributeStatement.Attribute; + } else if (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:AttributeStatement'] && assertion['saml:Assertion']['saml:AttributeStatement']['saml:Attribute']) { + attributes = assertion['saml:Assertion']['saml:AttributeStatement']['saml:Attribute']; + } - if (assertion.Subject.NameID) { - claims[nameIdentifierClaimType] = assertion.Subject.NameID; - } + if (attributes) { + attributes = (attributes instanceof Array) ? attributes : [attributes]; + attributes.forEach(function (attribute) { + claims[attribute['@'].Name] = attribute.AttributeValue || attribute['saml:AttributeValue']; + }); + } + } - return { - claims: claims, - issuer: assertion.Issuer - } + if (assertion.Subject && assertion.Subject.NameID) { + claims[nameIdentifierClaimType] = assertion.Subject.NameID; + } else if (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Subject'] && assertion['saml:Assertion']['saml:Subject']['NameID']) { + claims[nameIdentifierClaimType] = assertion['saml:Assertion']['saml:Subject']['NameID']; + } + + return { + claims : claims, + issuer : assertion.Issuer + || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Issuer'] + ? assertion['saml:Assertion']['saml:Issuer'] + : undefined) + } }; saml20.validateAudience = function (assertion, realm) { - return assertion.Conditions.AudienceRestriction.Audience === realm; + return (assertion.Conditions && assertion.Conditions.AudienceRestriction && assertion.Conditions.AudienceRestriction.Audience && assertion.Conditions.AudienceRestriction.Audience === realm) + || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Conditions'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience'] === realm); }; saml20.validateExpiration = function (assertion) { - var notBefore = new Date(assertion.Conditions['@'].NotBefore); - notBefore = notBefore.setMinutes(notBefore.getMinutes() - 10); // 10 minutes clock skew - var notOnOrAfter = new Date(assertion.Conditions['@'].NotOnOrAfter); - notOnOrAfter = notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 10); // 10 minutes clock skew + var dteNotBefore = (assertion.Conditions && assertion.Conditions['@'] && assertion.Conditions['@'].NotBefore + ? assertion.Conditions['@'].NotBefore + : (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Conditions'] && assertion['saml:Assertion']['saml:Conditions']['@'] && assertion['saml:Assertion']['saml:Conditions']['@']['NotBefore'] + ? assertion['saml:Assertion']['saml:Conditions']['@']['NotBefore'] + : undefined)); + var notBefore = new Date(dteNotBefore); + notBefore = notBefore.setMinutes(notBefore.getMinutes() - 10); // 10 minutes clock skew + + + var dteNotOnOrAfter = (assertion.Conditions && assertion.Conditions['@'] && assertion.Conditions['@'].NotOnOrAfter + ? assertion.Conditions['@'].NotOnOrAfter + : (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Conditions'] && assertion['saml:Assertion']['saml:Conditions']['@'] && assertion['saml:Assertion']['saml:Conditions']['@']['NotOnOrAfter'] + ? assertion['saml:Assertion']['saml:Conditions']['@']['NotOnOrAfter'] + : undefined)); + var notOnOrAfter = new Date(dteNotOnOrAfter); + notOnOrAfter = notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 10); // 10 minutes clock skew - var now = new Date(); - return !(now < notBefore || now > notOnOrAfter) - } \ No newline at end of file + var now = new Date(); + return !(now < notBefore || now > notOnOrAfter) +}; \ No newline at end of file diff --git a/lib/validateSignature.js b/lib/validateSignature.js index 9b42e0f..a07043c 100644 --- a/lib/validateSignature.js +++ b/lib/validateSignature.js @@ -5,7 +5,7 @@ var xmldom = require('xmldom'); module.exports = function (xml, cert, thumbprint) { var doc = new xmldom.DOMParser().parseFromString(xml); - var signature = xmlCrypto.xpath.SelectNodes(doc, "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; + var signature = xmlCrypto.xpath.SelectNodes(doc, "/*/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; var signed = new xmlCrypto.SignedXml(null, { idAttribute: 'AssertionID' }); From bba87892ab81be9016a3a17d0a1e5bfad28448a7 Mon Sep 17 00:00:00 2001 From: athieme Date: Fri, 13 Jun 2014 15:28:18 -0400 Subject: [PATCH 2/7] removed lodash --- lib/saml20.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/saml20.js b/lib/saml20.js index a6899aa..a701302 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -1,7 +1,6 @@ var nameIdentifierClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'; var saml20 = module.exports; -var _ = require('lodash'); saml20.parse = function (assertion) { From c3bcaf47f55be5960e33a6a881a98b5b819a3fbb Mon Sep 17 00:00:00 2001 From: athieme Date: Thu, 3 Jul 2014 09:19:20 -0400 Subject: [PATCH 3/7] fixed bug where prefix to NameId was missing which means the NameId would not be added as a claim --- lib/saml20.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/saml20.js b/lib/saml20.js index a701302..acd359d 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -26,8 +26,8 @@ saml20.parse = function (assertion) { if (assertion.Subject && assertion.Subject.NameID) { claims[nameIdentifierClaimType] = assertion.Subject.NameID; - } else if (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Subject'] && assertion['saml:Assertion']['saml:Subject']['NameID']) { - claims[nameIdentifierClaimType] = assertion['saml:Assertion']['saml:Subject']['NameID']; + } else if (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Subject'] && assertion['saml:Assertion']['saml:Subject']['saml:NameID']) { + claims[nameIdentifierClaimType] = assertion['saml:Assertion']['saml:Subject']['saml:NameID']; } return { From 694983b078087124321ce05f96c2e95713cd42e1 Mon Sep 17 00:00:00 2001 From: Chris Mordue Date: Fri, 11 Jul 2014 13:17:52 -0700 Subject: [PATCH 4/7] Add audience field to parsed results --- lib/saml11.js | 11 ++++++++++- lib/saml20.js | 16 ++++++++++++++-- test/lib.index.js | 17 +++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/saml11.js b/lib/saml11.js index 91caa47..0ac60a9 100644 --- a/lib/saml11.js +++ b/lib/saml11.js @@ -28,12 +28,21 @@ saml11.parse = function (assertion) { return { claims: claims, + audience : getAudience(assertion), issuer: assertion['@'].Issuer } } +function getAudience(assertion) { + if (assertion['saml:Conditions'] && assertion['saml:Conditions']['saml:AudienceRestrictionCondition']) { + return assertion['saml:Conditions']['saml:AudienceRestrictionCondition']['saml:Audience'] + } else { + return undefined; + } +} + saml11.validateAudience = function(assertion, realm) { - return assertion['saml:Conditions']['saml:AudienceRestrictionCondition']['saml:Audience'] === realm; + return getAudience(assertion) === realm; } saml11.validateExpiration = function (assertion) { diff --git a/lib/saml20.js b/lib/saml20.js index acd359d..1adae33 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -32,6 +32,7 @@ saml20.parse = function (assertion) { return { claims : claims, + audience : getAudience(assertion), issuer : assertion.Issuer || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Issuer'] ? assertion['saml:Assertion']['saml:Issuer'] @@ -39,9 +40,20 @@ saml20.parse = function (assertion) { } }; +function getAudience(assertion) { + if (assertion.Conditions && assertion.Conditions.AudienceRestriction && assertion.Conditions.AudienceRestriction.Audience) { + return assertion.Conditions.AudienceRestriction.Audience; + } else if (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Conditions'] + && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction'] + && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience']) { + return assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience']; + } else { + return undefined; + } +} + saml20.validateAudience = function (assertion, realm) { - return (assertion.Conditions && assertion.Conditions.AudienceRestriction && assertion.Conditions.AudienceRestriction.Audience && assertion.Conditions.AudienceRestriction.Audience === realm) - || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Conditions'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience'] && assertion['saml:Assertion']['saml:Conditions']['saml:AudienceRestriction']['saml:Audience'] === realm); + return getAudience(assertion) === realm; }; saml20.validateExpiration = function (assertion) { diff --git a/test/lib.index.js b/test/lib.index.js index 15201c0..96fc4f0 100644 --- a/test/lib.index.js +++ b/test/lib.index.js @@ -10,37 +10,41 @@ var issuerName = 'https://your-issuer.com'; var thumbprint = '1aeabdfa4473ecc7efc5947b19436c575574baf8'; var certificate = 'MIICDzCCAXygAwIBAgIQVWXAvbbQyI5BcFe0ssmeKTAJBgU...'; var audience = 'http://your-service.com/'; +var bypassExpiration = true; describe('SAML 2.0', function() { it("Should validate saml 2.0 token using thumbprint", function (done) { - saml.validate(validToken, { thumbprint: thumbprint, bypassExpiration: false }, function(err, profile) { + saml.validate(validToken, { thumbprint: thumbprint, bypassExpiration: bypassExpiration }, function(err, profile) { assert.ifError(err); assert.equal(issuerName, profile.issuer); + assert.equal(audience, profile.audience); assert.ok(profile.claims); done(); }) }); it("Should validate saml 2.0 token using certificate", function (done) { - saml.validate(validToken, { publicKey: certificate, bypassExpiration: false }, function(err, profile) { + saml.validate(validToken, { publicKey: certificate, bypassExpiration: bypassExpiration }, function(err, profile) { assert.ifError(err); assert.equal(issuerName, profile.issuer); + assert.equal(audience, profile.audience); assert.ok(profile.claims); done(); }) }); it("Should validate saml 2.0 token and check audience", function (done) { - saml.validate(validToken, { publicKey: certificate, audience: audience, bypassExpiration: false }, function(err, profile) { + saml.validate(validToken, { publicKey: certificate, audience: audience, bypassExpiration: bypassExpiration }, function(err, profile) { assert.ifError(err); assert.equal(issuerName, profile.issuer); + assert.equal(audience, profile.audience); assert.ok(profile.claims); done(); }) }); it("Should fail with invalid audience", function (done) { - saml.validate(validToken, { publicKey: certificate, audience: 'http://any-other-audience.com/', bypassExpiration: false }, function(err, profile) { + saml.validate(validToken, { publicKey: certificate, audience: 'http://any-other-audience.com/', bypassExpiration: bypassExpiration }, function(err, profile) { assert.ok(!profile); assert.ok(err); assert.equal('Invalid audience.', err.message); @@ -49,7 +53,7 @@ describe('SAML 2.0', function() { }); it("Should fail with invalid signature", function (done) { - saml.validate(invalidToken, { publicKey: certificate, bypassExpiration: false }, function(err, profile) { + saml.validate(invalidToken, { publicKey: certificate, bypassExpiration: bypassExpiration }, function(err, profile) { assert.ok(!profile); assert.ok(err); assert.equal('Invalid assertion signature.', err.message); @@ -58,7 +62,7 @@ describe('SAML 2.0', function() { }); it("Should fail with invalid assertion", function (done) { - saml.validate('invalid-assertion', { publicKey: certificate, bypassExpiration: false }, function(err, profile) { + saml.validate('invalid-assertion', { publicKey: certificate, bypassExpiration: bypassExpiration }, function(err, profile) { assert.ok(!profile); assert.ok(err); assert.equal('Invalid assertion.', err.message); @@ -70,6 +74,7 @@ describe('SAML 2.0', function() { saml.parse(invalidToken, function(err, profile) { assert.ifError(err); assert.equal(issuerName, profile.issuer); + assert.equal(audience, profile.audience); assert.ok(profile.claims); done(); }) From 7e55102d1692e4cf172d5339916e52a8adcf26b6 Mon Sep 17 00:00:00 2001 From: Chris Mordue Date: Fri, 1 Aug 2014 01:44:01 -0700 Subject: [PATCH 5/7] Validate Signatures on either the saml Response or the saml Assertion. --- lib/validateSignature.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/validateSignature.js b/lib/validateSignature.js index a07043c..1cd7a4d 100644 --- a/lib/validateSignature.js +++ b/lib/validateSignature.js @@ -5,7 +5,8 @@ var xmldom = require('xmldom'); module.exports = function (xml, cert, thumbprint) { var doc = new xmldom.DOMParser().parseFromString(xml); - var signature = xmlCrypto.xpath.SelectNodes(doc, "/*/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; + var signature = xmlCrypto.xpath.SelectNodes(doc, "/*/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0] + || xmlCrypto.xpath.SelectNodes(doc, "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; var signed = new xmlCrypto.SignedXml(null, { idAttribute: 'AssertionID' }); From eeaf3abd462830dc7838ca95d353a42f372fb0de Mon Sep 17 00:00:00 2001 From: Chris Mordue Date: Fri, 24 Apr 2015 18:39:35 -0700 Subject: [PATCH 6/7] Parse SessionIndex from assertion --- lib/saml20.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/saml20.js b/lib/saml20.js index 1adae33..41cb526 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -36,7 +36,8 @@ saml20.parse = function (assertion) { issuer : assertion.Issuer || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Issuer'] ? assertion['saml:Assertion']['saml:Issuer'] - : undefined) + : undefined), + sessionIndex : getSessionIndex(assertion) } }; @@ -52,6 +53,10 @@ function getAudience(assertion) { } } +function getSessionIndex(assertion) { + return assertion.AuthnStatement && assertion.AuthnStatement['@'] && assertion.AuthnStatement['@'].SessionIndex; +} + saml20.validateAudience = function (assertion, realm) { return getAudience(assertion) === realm; }; From 928609047660b6bb493941dea2ddaf79c4bd8d96 Mon Sep 17 00:00:00 2001 From: Chris Mordue Date: Tue, 28 Apr 2015 10:10:22 -0700 Subject: [PATCH 7/7] Fixed namespaces in parse SessionIndex from assertion --- lib/saml20.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/saml20.js b/lib/saml20.js index 41cb526..8b32b54 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -54,7 +54,7 @@ function getAudience(assertion) { } function getSessionIndex(assertion) { - return assertion.AuthnStatement && assertion.AuthnStatement['@'] && assertion.AuthnStatement['@'].SessionIndex; + return assertion['saml:Assertion']['saml:AuthnStatement'] && assertion['saml:Assertion']['saml:AuthnStatement']['@'] && assertion['saml:Assertion']['saml:AuthnStatement']['@'].SessionIndex; } saml20.validateAudience = function (assertion, realm) {