Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/saml11.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
92 changes: 67 additions & 25 deletions lib/saml20.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
var now = new Date();
return !(now < notBefore || now > notOnOrAfter)
};
3 changes: 2 additions & 1 deletion lib/validateSignature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });

Expand Down
17 changes: 11 additions & 6 deletions test/lib.index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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();
})
Expand Down