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 c00b25a..8b32b54 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -3,41 +3,83 @@ var nameIdentifierClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/cl var saml20 = module.exports; 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']['saml:NameID']) { + claims[nameIdentifierClaimType] = assertion['saml:Assertion']['saml:Subject']['saml:NameID']; + } + + return { + claims : claims, + audience : getAudience(assertion), + issuer : assertion.Issuer + || (assertion['saml:Assertion'] && assertion['saml:Assertion']['saml:Issuer'] + ? assertion['saml:Assertion']['saml:Issuer'] + : undefined), + sessionIndex : getSessionIndex(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; + } +} + +function getSessionIndex(assertion) { + return assertion['saml:Assertion']['saml:AuthnStatement'] && assertion['saml:Assertion']['saml:AuthnStatement']['@'] && assertion['saml:Assertion']['saml:AuthnStatement']['@'].SessionIndex; +} + saml20.validateAudience = function (assertion, realm) { - return assertion.Conditions.AudienceRestriction.Audience === realm; + return getAudience(assertion) === 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..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' }); 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(); })