diff --git a/lib/index.js b/lib/index.js index 97f495b0..1ccd3470 100644 --- a/lib/index.js +++ b/lib/index.js @@ -71,6 +71,11 @@ saml.validate = function validate(rawAssertion, options, cb) { cb(new Error('Invalid audience.')); return; } + + if (options.inResponseTo && assertion.inResponseTo !== options.inResponseTo) { + cb(new Error('Invalid InResponseTo.')); + return; + } parseAttributes(assertion, tokenHandler, cb); }); @@ -101,6 +106,9 @@ function parseXmlAndVersion (rawAssertion, cb) { return; } + var tokenHandler = tokenHandlers[version]; + assertion.inResponseTo = tokenHandler.getInResponseTo(xml); + cb(null, assertion, version); }); } diff --git a/lib/saml11.js b/lib/saml11.js index e5b9260b..58f84600 100644 --- a/lib/saml11.js +++ b/lib/saml11.js @@ -60,3 +60,7 @@ saml11.validateExpiration = function validateExpiration(assertion) { return !(now < notBefore || now > notOnOrAfter); }; + + saml11.getInResponseTo = function validateAudience(xml) { + return _.get(xml, 'Response.@.ResponseID'); +}; diff --git a/lib/saml20.js b/lib/saml20.js index 3a85b800..e7d4cfe3 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -96,3 +96,7 @@ saml20.validateExpiration = function validateExpiration(assertion) { var now = new Date(); return !(now < notBefore || now > notOnOrAfter); }; + +saml20.getInResponseTo = function validateAudience(xml) { + return getProp(xml, 'Response.@.InResponseTo'); +}; diff --git a/package-lock.json b/package-lock.json index 74f44ecb..bd0ca9b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@boxyhq/saml20", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@boxyhq/saml20", - "version": "0.1.5", + "version": "0.1.7", "license": "MIT", "dependencies": { - "@xmldom/xmldom": "0.7.2", + "@xmldom/xmldom": "0.7.4", "lodash": "^4.17.21", "thumbprint": "^0.0.1", "xml-crypto": "^2.1.3", @@ -27,9 +27,9 @@ "dev": true }, "node_modules/@xmldom/xmldom": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.2.tgz", - "integrity": "sha512-t/Zqo0ewes3iq6zGqEqJNUWI27Acr3jkmSUNp6E3nl0Z2XbtqAG5XYqPNLdYonILmhcxANsIidh69tHzjXtuRg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.4.tgz", + "integrity": "sha512-wdxC79cvO7PjSM34jATd/RYZuYWQ8y/R7MidZl1NYYlbpFn1+spfjkiR3ZsJfcaTs2IyslBN7VwBBJwrYKM+zw==", "engines": { "node": ">=10.0.0" } @@ -1330,9 +1330,9 @@ "dev": true }, "@xmldom/xmldom": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.2.tgz", - "integrity": "sha512-t/Zqo0ewes3iq6zGqEqJNUWI27Acr3jkmSUNp6E3nl0Z2XbtqAG5XYqPNLdYonILmhcxANsIidh69tHzjXtuRg==" + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.4.tgz", + "integrity": "sha512-wdxC79cvO7PjSM34jATd/RYZuYWQ8y/R7MidZl1NYYlbpFn1+spfjkiR3ZsJfcaTs2IyslBN7VwBBJwrYKM+zw==" }, "ansi-colors": { "version": "4.1.1", diff --git a/package.json b/package.json index cec6df97..19cbabc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@boxyhq/saml20", - "version": "0.1.6", + "version": "0.1.7", "description": "SAML 2.0 and 1.1 token parser for Node.js", "main": "./lib/index.js", "dependencies": { @@ -8,7 +8,7 @@ "thumbprint": "^0.0.1", "xml-crypto": "^2.1.3", "xml2js": "^0.4.23", - "@xmldom/xmldom": "0.7.2" + "@xmldom/xmldom": "0.7.4" }, "repository": { "type": "git", diff --git a/test/assets/saml20.validResponse.xml b/test/assets/saml20.validResponse.xml new file mode 100644 index 00000000..1faf96bc --- /dev/null +++ b/test/assets/saml20.validResponse.xml @@ -0,0 +1,41 @@ + + http://idp.example.com/metadata.php + + + + + http://idp.example.com/metadata.php + + + LvGS1oVn0GClCCwpeiuMKHdZkN4=JmHdhq38jnSpYNn4EFbRVCF49ankAdBwQ+5gatF3ZFF3QHc7UuZ8k8azNSJAxuE1PqSmVr+abtX/0roICJyUcTRJkh/1BGWX8LG5njEHe4MVoRWmRzXwHzYaXB+cRbH05ZUufZR30yfolJTEbXPJJ7Le35rXgDJoc7P3JXyAKoo= +MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://sp.example.com/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + + \ No newline at end of file diff --git a/test/lib.saml20.response.js b/test/lib.saml20.response.js new file mode 100644 index 00000000..1fa36813 --- /dev/null +++ b/test/lib.saml20.response.js @@ -0,0 +1,136 @@ +var assert = require("assert"); +var fs = require("fs"); +var saml = require("../lib/index.js"); + +// Tests Configuration +var validResponse = fs + .readFileSync('./test/assets/saml20.validResponse.xml') + .toString(); + +var issuerName = 'http://idp.example.com/metadata.php'; +var thumbprint = 'e606eced42fa3abd0c5693456384f5931b174707'; +var certificate = 'MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg=='; +var audience = 'http://sp.example.com/demo1/metadata.php'; +var inResponseTo = 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'; + +describe("lib.saml20.response", function () { + it("Should validate saml 2.0 token using thumbprint", function (done) { + saml.validate( + validResponse, + { + publicKey: certificate, + thumbprint: thumbprint, + bypassExpiration: true, + inResponseTo: inResponseTo, + }, + function (err, profile) { + assert.ifError(err); + assert.ok(profile.claims); + + assert.strictEqual(issuerName, profile.issuer); + assert.strictEqual( + '_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7', + profile.claims[ + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' + ] + ); + done(); + } + ); + }); + + it("Should validate saml 2.0 token using certificate", function (done) { + saml.validate( + validResponse, + { + publicKey: certificate, + bypassExpiration: true, + inResponseTo: inResponseTo, + }, + function (err, profile) { + assert.ifError(err); + assert.strictEqual(issuerName, profile.issuer); + assert.ok(profile.claims); + assert.strictEqual( + '_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7', + profile.claims[ + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' + ] + ); + + done(); + } + ); + }); + + it("Should validate saml 2.0 token and check audience", function (done) { + saml.validate( + validResponse, + { + publicKey: certificate, + audience: audience, + bypassExpiration: true, + inResponseTo: inResponseTo, + }, + function (err, profile) { + assert.ifError(err); + assert.strictEqual(issuerName, profile.issuer); + assert.ok(profile.claims); + done(); + } + ); + }); + + it("Should fail with invalid audience", function (done) { + saml.validate( + validResponse, + { + publicKey: certificate, + audience: 'http://any-other-audience.com/', + bypassExpiration: true, + inResponseTo: inResponseTo, + }, + function (err, profile) { + assert.ok(!profile); + assert.ok(err); + assert.strictEqual('Invalid audience.', err.message); + done(); + } + ); + }); + + it("Should fail with invalid assertion", function (done) { + saml.validate( + 'invalid-assertion', + { + publicKey: certificate, + bypassExpiration: true, + inResponseTo: inResponseTo, + }, + function (err, profile) { + assert.ok(!profile); + assert.ok(err); + assert.strictEqual('Invalid assertion.', err.message); + done(); + } + ); + }); + + it("Should fail with invalid inResponseTo", function (done) { + saml.validate( + validResponse, + { + publicKey: certificate, + audience: audience, + bypassExpiration: true, + inResponseTo: 'not-the-right-response-to', + }, + function (err, profile) { + assert.ok(!profile); + assert.ok(err); + assert.strictEqual('Invalid InResponseTo.', err.message); + done(); + } + ); + }); +});