├── .gitignore ├── .travis.yml ├── lib ├── utils.js ├── index.js ├── templates.js ├── interpolate.js ├── encoders.js ├── federationServerService.js ├── metadata.js ├── claims │ └── PassportProfileMapper.js └── wsfed.js ├── test ├── interpolate.tests.js ├── fixture │ ├── wsfed.test-cert.pub │ ├── wsfed.test-cert.pem │ ├── wsfed.test-cert.pb7 │ ├── wsfed.test-cert.key │ └── server.js ├── custom_form.html ├── wsfed-sha1.tests.js ├── metadata.tests.js ├── jwt.tests.js ├── wsfed-encryption.tests.js ├── xmlhelper.js ├── federationServerService.tests.js ├── wsfed.tests.js └── wsfed.custom_form.tests.js ├── templates ├── form_el.ejs ├── form.ejs ├── federationServerServiceWsdl.ejs ├── federationServerServiceResponse.ejs ├── metadata.ejs └── federationServerService.ejs ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | exports.escape = function(html) { 3 | return String(html) 4 | .replace(/&/g, '&') 5 | .replace(//g, '>') 7 | .replace(/"/g, '"'); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.auth = require('./wsfed'); 2 | exports.metadata = require('./metadata'); 3 | exports.federationServerService = {}; 4 | exports.federationServerService.wsdl = require('./federationServerService').wsdl; 5 | exports.federationServerService.thumbprint = require('./federationServerService').thumbprint; -------------------------------------------------------------------------------- /test/interpolate.tests.js: -------------------------------------------------------------------------------- 1 | var interpolate = require('../lib/interpolate'); 2 | var expect = require('chai').expect; 3 | 4 | describe('interpolation template', function () { 5 | it('should work', function () { 6 | var r = interpolate('aaa@@test@@')({ 7 | test:'bbb' 8 | }); 9 | expect(r).to.equal('aaabbb'); 10 | }); 11 | }); -------------------------------------------------------------------------------- /lib/templates.js: -------------------------------------------------------------------------------- 1 | var ejs = require('ejs'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var templates = fs.readdirSync(path.join(__dirname, '../templates')); 6 | 7 | templates.forEach(function (tmplFile) { 8 | var content = fs.readFileSync(path.join(__dirname, '../templates', tmplFile)); 9 | var template = ejs.compile(content.toString()); 10 | exports[tmplFile.slice(0, -4)] = template; 11 | }); -------------------------------------------------------------------------------- /templates/form_el.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 12 |
-------------------------------------------------------------------------------- /test/fixture/wsfed.test-cert.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtH4wKLYlIXZlfYQFJtX 3 | ZVC3fD8XMarzwvb/fHUyJ6NvNStN+H7GHp3/QhZbSaRyqK5hu5xXtFLgnI0QG8oE 4 | 1NlXbczjH45LeHWhPIdc2uHSpzXic78kOugMY1vng4J10PF6+T2FNaiv0iXeIQq9 5 | xbwwPYpflViQyJnzGCIZ7VGan6GbRKzyTKcB58yx24pJq+CviLXEY52TIW1l5imc 6 | jGvLtlCp1za9qBZa4XGoVqHi1kRXkdDSHty6lZWj3KxoRvTbiaBCH+75U7rifS6f 7 | R9lqjWE57bCGoz7+BBu9YmPKtI1KkyHFqWpxaJc/AKf9xgg+UumeqVcirUmAsHJr 8 | MwIDAQAB 9 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /lib/interpolate.js: -------------------------------------------------------------------------------- 1 | function getProp(obj, path) { 2 | return path.split('.').reduce(function (prev, curr) { 3 | return prev[curr]; 4 | }, obj); 5 | } 6 | 7 | function escape (html){ 8 | return String(html) 9 | .replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&') 10 | .replace(//g, '>') 12 | .replace(/'/g, ''') 13 | .replace(/"/g, '"'); 14 | } 15 | 16 | module.exports = function (tmpl) { 17 | return function (model) { 18 | return tmpl.replace(/\@\@([^\@]*)\@\@/g, 19 | function (a, b) { 20 | var r = getProp(model, b); 21 | var value = typeof r === 'string' || typeof r === 'number' ? r : a; 22 | return escape(value); 23 | } 24 | ); 25 | }; 26 | }; -------------------------------------------------------------------------------- /test/custom_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Working... 4 | 5 | 6 |
7 | 8 | 11 | 12 | 17 |
18 | 21 | 22 | -------------------------------------------------------------------------------- /templates/form.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Working... 4 | 5 | 6 |
7 | 8 | 11 | 12 | 17 |
18 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsfed", 3 | "version": "1.0.3", 4 | "description": "WSFed server middleware", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/auth0/node-wsfed.git" 12 | }, 13 | "keywords": [ 14 | "wsfed", 15 | "saml", 16 | "auth" 17 | ], 18 | "author": "Auth0", 19 | "license": "mit", 20 | "dependencies": { 21 | "ejs": "~0.8.3", 22 | "thumbprint": "0.0.1", 23 | "saml": "~0.6.1", 24 | "jsonwebtoken": "~0.4.1" 25 | }, 26 | "devDependencies": { 27 | "chai": "~1.5.0", 28 | "express": "~3.1.0", 29 | "mocha": "~1.8.1", 30 | "request": "~2.14.0", 31 | "xmldom": "~0.1.13", 32 | "cheerio": "~0.10.7", 33 | "xml-crypto": "0.0.10", 34 | "xpath": "0.0.5", 35 | "xtend": "~2.0.3", 36 | "xml-encryption": "~0.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 AUTH10 LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/encoders.js: -------------------------------------------------------------------------------- 1 | var thumbprint = require('thumbprint'); 2 | 3 | var removeHeaders = module.exports.removeHeaders = function (cert) { 4 | var pem = /-----BEGIN (\w*)-----([^-]*)-----END (\w*)-----/g.exec(cert.toString()); 5 | if (pem && pem.length > 0) { 6 | return pem[2].replace(/[\n|\r\n]/g, ''); 7 | } 8 | return null; 9 | }; 10 | 11 | module.exports.thumbprint = function (pem) { 12 | var cert = removeHeaders(pem); 13 | return thumbprint.calculate(cert).toUpperCase(); 14 | }; 15 | 16 | module.exports.toCertifiedStore = function (pem) { 17 | var cert = removeHeaders(pem); 18 | var certBuffer = new Buffer(cert, 'base64'); 19 | 20 | var header = new Buffer(8); 21 | header.writeUInt32LE(0x00000000, 0); 22 | header.writeUInt32LE(0x54524543, 4); 23 | 24 | 25 | var start = new Buffer(12); 26 | start.writeUInt32LE(0x00000020, 0); 27 | start.writeUInt32LE(0x00000001, 4); 28 | start.writeUInt32LE(certBuffer.length, 8); 29 | 30 | var ending = new Buffer(12); 31 | ending.writeUInt32LE(0x00000000, 0); 32 | ending.writeUInt32LE(0x00000000, 4); 33 | 34 | return Buffer.concat([header, start, certBuffer, ending]).toString('base64'); 35 | }; -------------------------------------------------------------------------------- /templates/federationServerServiceWsdl.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixture/wsfed.test-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJAMKR/NsyfcazMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTIxMTEyMjM0MzQxWhcNMTYxMjIxMjM0MzQxWjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN+H7GHp3/ 8 | QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXic78kOugM 9 | Y1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6GbRKzyTKcB 10 | 58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRXkdDSHty6 11 | lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1KkyHFqWpx 12 | aJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABo4GnMIGkMB0GA1UdDgQWBBTs83nk 13 | LtoXFlmBUts3EIxcVvkvcjB1BgNVHSMEbjBsgBTs83nkLtoXFlmBUts3EIxcVvkv 14 | cqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAMKR/NsyfcazMAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABw7w/5k4d5dVDgd/OOOmXdaaCIKvt7d 17 | 3ntlv1SSvAoKT8d8lt97Dm5RrmefBI13I2yivZg5bfTge4+vAV6VdLFdWeFp1b/F 18 | OZkYUv6A8o5HW0OWQYVX26zIqBcG2Qrm3reiSl5BLvpj1WSpCsYvs5kaO4vFpMak 19 | /ICgdZD+rxwxf8Vb/6fntKywWSLgwKH3mJ+Z0kRlpq1g1oieiOm1/gpZ35s0Yuor 20 | XZba9ptfLCYSggg/qc3d3d0tbHplKYkwFm7f5ORGHDSD5SJm+gI7RPE+4bO8q79R 21 | PAfbG1UGuJ0b/oigagciHhJp851SQRYf3JuNSc17BnK2L5IEtzjqr+Q= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /templates/federationServerServiceResponse.ejs: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | true 9 | 10 | 1 11 | 26886a27-50ad-9695-3511-8d24a1a3a23b 12 | 1 13 | 14 | 15 | 16 | 17 | 18 | <%= thumbprint %> 19 | 20 | 21 | None 22 | 23 | 24 | <%= cert %> 25 | 26 | <%= location %> 27 | <%= location %> 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/fixture/wsfed.test-cert.pb7: -------------------------------------------------------------------------------- 1 | -----BEGIN PKCS7----- 2 | MIID5gYJKoZIhvcNAQcCoIID1zCCA9MCAQExADALBgkqhkiG9w0BBwGgggO5MIID 3 | tTCCAp2gAwIBAgIJAMKR/NsyfcazMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYT 4 | AkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 5 | aXRzIFB0eSBMdGQwHhcNMTIxMTEyMjM0MzQxWhcNMTYxMjIxMjM0MzQxWjBFMQsw 6 | CQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJu 7 | ZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 8 | AQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN+H7GHp3/QhZb 9 | SaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXic78kOugMY1vn 10 | g4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6GbRKzyTKcB58yx 11 | 24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRXkdDSHty6lZWj 12 | 3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1KkyHFqWpxaJc/ 13 | AKf9xgg+UumeqVcirUmAsHJrMwIDAQABo4GnMIGkMB0GA1UdDgQWBBTs83nkLtoX 14 | FlmBUts3EIxcVvkvcjB1BgNVHSMEbjBsgBTs83nkLtoXFlmBUts3EIxcVvkvcqFJ 15 | pEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT 16 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAMKR/NsyfcazMAwGA1UdEwQFMAMB 17 | Af8wDQYJKoZIhvcNAQEFBQADggEBABw7w/5k4d5dVDgd/OOOmXdaaCIKvt7d3ntl 18 | v1SSvAoKT8d8lt97Dm5RrmefBI13I2yivZg5bfTge4+vAV6VdLFdWeFp1b/FOZkY 19 | Uv6A8o5HW0OWQYVX26zIqBcG2Qrm3reiSl5BLvpj1WSpCsYvs5kaO4vFpMak/ICg 20 | dZD+rxwxf8Vb/6fntKywWSLgwKH3mJ+Z0kRlpq1g1oieiOm1/gpZ35s0YuorXZba 21 | 9ptfLCYSggg/qc3d3d0tbHplKYkwFm7f5ORGHDSD5SJm+gI7RPE+4bO8q79RPAfb 22 | G1UGuJ0b/oigagciHhJp851SQRYf3JuNSc17BnK2L5IEtzjqr+ShADEA 23 | -----END PKCS7----- 24 | -------------------------------------------------------------------------------- /lib/federationServerService.js: -------------------------------------------------------------------------------- 1 | var templates = require('./templates'); 2 | var URL_PATH = '/wsfed/adfs/fs/federationserverservice.asmx'; 3 | var encoders = require('./encoders'); 4 | 5 | function getLocation (req) { 6 | var protocol = req.headers['x-iisnode-https'] && req.headers['x-iisnode-https'] == 'ON' ? 7 | 'https' : 8 | (req.headers['x-forwarded-proto'] || req.protocol); 9 | 10 | return protocol + '://' + req.headers['host'] + req.originalUrl; 11 | } 12 | 13 | function getEndpointAddress (req, endpointPath) { 14 | endpointPath = endpointPath || 15 | (req.originalUrl.substr(0, req.originalUrl.length - URL_PATH.length)); 16 | 17 | var protocol = req.headers['x-iisnode-https'] && req.headers['x-iisnode-https'] == 'ON' ? 18 | 'https' : 19 | (req.headers['x-forwarded-proto'] || req.protocol); 20 | 21 | return protocol + '://' + req.headers['host'] + endpointPath; 22 | } 23 | 24 | 25 | module.exports.wsdl = function (req, res) { 26 | res.set('Content-Type', 'text/xml; charset=UTF-8'); 27 | if(req.query.wsdl){ 28 | return res.send(templates.federationServerServiceWsdl()); 29 | } 30 | res.send(templates.federationServerService({ 31 | location: getLocation(req) 32 | })); 33 | }; 34 | 35 | module.exports.thumbprint = function (options) { 36 | return function (req, res) { 37 | res.set('Content-Type', 'text/xml; charset=UTF-8'); 38 | res.send(templates.federationServerServiceResponse({ 39 | location: getEndpointAddress(req, options.endpointPath), 40 | cert: encoders.removeHeaders(options.pkcs7.toString()), 41 | thumbprint: encoders.thumbprint(options.cert) 42 | })); 43 | }; 44 | }; -------------------------------------------------------------------------------- /test/wsfed-sha1.tests.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var server = require('./fixture/server'); 3 | var request = require('request'); 4 | var cheerio = require('cheerio'); 5 | var xmlhelper = require('./xmlhelper'); 6 | 7 | describe('wsfed with sha1', function () { 8 | before(function (done) { 9 | server.start({ 10 | signatureAlgorithm: 'rsa-sha1', 11 | digestAlgorithm: 'sha1' 12 | }, done); 13 | }); 14 | 15 | after(function (done) { 16 | server.close(done); 17 | }); 18 | 19 | describe('authorizing', function () { 20 | var body, $, signedAssertion, attributes; 21 | 22 | before(function (done) { 23 | request.get({ 24 | jar: request.jar(), 25 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:the-super-client-id' 26 | }, function (err, response, b){ 27 | if(err) return done(err); 28 | body = b; 29 | $ = cheerio.load(body); 30 | var wresult = $('input[name="wresult"]').attr('value'); 31 | signedAssertion = /(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 32 | attributes = xmlhelper.getAttributes(signedAssertion); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should use sha1 as signature algorithm', function(){ 38 | var algorithm = xmlhelper.getSignatureMethodAlgorithm(signedAssertion); 39 | expect(algorithm).to.equal('http://www.w3.org/2000/09/xmldsig#rsa-sha1'); 40 | }); 41 | 42 | it('should use sha1 as digest algorithm', function(){ 43 | var algorithm = xmlhelper.getDigestMethodAlgorithm(signedAssertion); 44 | expect(algorithm).to.equal('http://www.w3.org/2000/09/xmldsig#sha1'); 45 | }); 46 | 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/fixture/wsfed.test-cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN 3 | +H7GHp3/QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXi 4 | c78kOugMY1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6Gb 5 | RKzyTKcB58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRX 6 | kdDSHty6lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1K 7 | kyHFqWpxaJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABAoIBAQCYKw05YSNhXVPk 8 | eHLeW/pXuwR3OkCexPrakOmwMC0s2vIF7mChN0d6hvhVlUp68X7V8SnS2JxAGo8v 9 | iHY+Et3DdwZ3cxnzwh+BEhzgDfoIOmkoGppZPyX/K6klWtbGUrTtSISOWXbvEXQU 10 | G0qGAvDOzIGTsdMDX7slnU70Ac23JybPY5qBSiE+ky8U4dm2fUHMroWub4QP5vA/ 11 | nqyWqX2FB/MEAbcujaknDQrFCtbmtUYlBbJCKGd9V3cGEqp6H7oH+ah2ofMc91gJ 12 | mCHk3YyWZB/bcVXH3CA+s1ywvCOVDBZ3Nw7Pt9zIcv6Rl9UKIy+Nx0QjXxR90Hla 13 | Tr0GHIShAoGBAPsD7uXm+0ksnGyKRYgvlVad8Z8FUFT6bf4B+vboDbx40FO8O/5V 14 | PraBPC5z8YRSBOQ/WfccPQzakkA28F2pXlRpXu5JcErVWnyyUiKpX5sw6iPenQR2 15 | JO9hY/GFbKiwUhVHpvWMcXFqFLSQu2A86jPnFFEfG48ZT4IhTzINKJVZAoGBAMKc 16 | B3YGfVfY9qiRFXzYRdSRLg5c8p/HzuWwXc9vfJ4kQTDkPXe/+nqD67rzeT54uVec 17 | jKoIrsCu4BfEaoyvOT+1KmUfdEpBgYZuuEC4CZf7dgKbXOpPVvZDMyJ/e7HyqTpw 18 | mvIYJLPm2fNAcAsnbrNX5mhLwwzEIltbplUUeRdrAoGBAKhZgPYsLkhrZRXevreR 19 | wkTvdUfD1pbHxtFfHqROCjhnhsFCM7JmFcNtdaFqHYczQxiZ7IqxI7jlNsVek2Md 20 | 3qgaa5LBKlDmOuP67N9WXUrGSaJ5ATIm0qrB1Lf9VlzktIiVH8L7yHHaRby8fQ8U 21 | i7b3ukaV6HPW895A3M6iyJ8xAoGAInp4S+3MaTL0SFsj/nFmtcle6oaHKc3BlyoP 22 | BMBQyMfNkPbu+PdXTjtvGTknouzKkX4X4cwWAec5ppxS8EffEa1sLGxNMxa19vZI 23 | yJaShI21k7Ko3I5f7tNrDNKfPKCsYMEwgnHKluDwfktNTnyW/Uk2dgXuMaXSHHN5 24 | XZt59K8CgYArGVOWK7LUmf3dkTIs3tXBm4/IMtUZmWmcP9C8Xe/Dg/IdQhK5CIx4 25 | VXl8rgZNeX/5/4nJ8Q3LrdLau1Iz620trNRGU6sGMs3x4WQbSq93RRbFzfG1oK74 26 | IOo5yIBxImQOSk5jz31gF9RJb15SDBIxonuWv8qAERyUfvrmEwR0kg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/metadata.tests.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var server = require('./fixture/server'); 3 | var request = require('request'); 4 | var xmldom = require('xmldom'); 5 | 6 | function certToPem (cert) { 7 | var pem = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(cert.toString()); 8 | if (pem.length > 0) { 9 | return pem[1].replace(/[\n|\r\n]/g, ''); 10 | } 11 | return null; 12 | } 13 | 14 | describe('wsfed metadata', function () { 15 | before(function (done) { 16 | server.start(done); 17 | }); 18 | 19 | after(function (done) { 20 | server.close(done); 21 | }); 22 | 23 | describe('request to metadata', function (){ 24 | var doc, content; 25 | before(function (done) { 26 | request.get({ 27 | jar: request.jar(), 28 | uri: 'http://localhost:5050/wsfed/FederationMetadata/2007-06/FederationMetadata.xml' 29 | }, function (err, response, b){ 30 | if(err) return done(err); 31 | content = b; 32 | doc = new xmldom.DOMParser().parseFromString(b).documentElement; 33 | done(); 34 | }); 35 | }); 36 | 37 | it('sholud have the endpoint url', function(){ 38 | expect(doc.getElementsByTagName('EndpointReference')[0].firstChild.textContent) 39 | .to.equal('http://localhost:5050/wsfed'); 40 | }); 41 | 42 | it('sholud have the claim types', function(){ 43 | expect(doc.getElementsByTagName('auth:ClaimType')) 44 | .to.not.be.empty; 45 | }); 46 | 47 | it('sholud have the issuer', function(){ 48 | expect(doc.getAttribute('entityID')) 49 | .to.equal('fixture-test'); 50 | }); 51 | 52 | it('sholud have the pem', function(){ 53 | expect(doc.getElementsByTagName('X509Certificate')[0].textContent) 54 | .to.equal(certToPem(server.credentials.cert)); 55 | }); 56 | 57 | it('should not contain line breaks', function(){ 58 | expect(content) 59 | .to.not.contain('\n'); 60 | }); 61 | 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/jwt.tests.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var expect = require('chai').expect; 4 | var server = require('./fixture/server'); 5 | var request = require('request'); 6 | var cheerio = require('cheerio'); 7 | var xmlhelper = require('./xmlhelper'); 8 | var jwt = require('jsonwebtoken'); 9 | 10 | var credentials = { 11 | cert: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.pem')), 12 | key: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.key')), 13 | pkcs7: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.pb7')) 14 | }; 15 | 16 | 17 | describe('wsfed+jwt', function () { 18 | before(function (done) { 19 | server.start({ 20 | jwt: true 21 | }, done); 22 | }); 23 | 24 | after(function (done) { 25 | server.close(done); 26 | }); 27 | 28 | describe('authorizing', function () { 29 | var body, $, signedAssertion, profile; 30 | 31 | before(function (done) { 32 | request.get({ 33 | jar: request.jar(), 34 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:the-super-client-id' 35 | }, function (err, response, b){ 36 | if(err) return done(err); 37 | body = b; 38 | $ = cheerio.load(body); 39 | var signedAssertion = $('input[name="wresult"]').attr('value'); 40 | jwt.verify(signedAssertion, credentials.cert.toString(), function (err, decoded) { 41 | if (err) return done(err); 42 | profile = decoded; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | it('should have the attributes', function(){ 49 | expect(profile).to.have.property('displayName'); 50 | expect(profile.id).to.equal('12334444444'); 51 | }); 52 | 53 | it('should have jwt attributes', function(){ 54 | expect(profile).to.have.property('aud'); 55 | expect(profile).to.have.property('iss'); 56 | expect(profile).to.have.property('iat'); 57 | }); 58 | 59 | }); 60 | }); -------------------------------------------------------------------------------- /test/wsfed-encryption.tests.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var expect = require('chai').expect; 4 | var server = require('./fixture/server'); 5 | var request = require('request'); 6 | var cheerio = require('cheerio'); 7 | var xmlenc = require('xml-encryption'); 8 | var xmlhelper = require('./xmlhelper'); 9 | 10 | var credentials = { 11 | cert: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.pem')), 12 | key: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.key')), 13 | pkcs7: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.pb7')), 14 | pub: fs.readFileSync(path.join(__dirname, '/fixture/wsfed.test-cert.pub')) 15 | }; 16 | 17 | 18 | describe('when dwdw encrypting the assertion', function () { 19 | before(function (done) { 20 | server.start({ 21 | encryptionPublicKey: credentials.pub, 22 | encryptionCert: credentials.cert 23 | }, done); 24 | }); 25 | 26 | after(function (done) { 27 | server.close(done); 28 | }); 29 | 30 | var body, $, encryptedAssertion; 31 | 32 | describe('when encrypting the assertion', function () { 33 | before(function (done) { 34 | request.get({ 35 | jar: request.jar(), 36 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:the-super-client-id' 37 | }, function (err, response, b){ 38 | if(err) return done(err); 39 | body = b; 40 | $ = cheerio.load(body); 41 | var wresult = $('input[name="wresult"]').attr('value'); 42 | encryptedAssertion = /(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should contain a form in the result', function(){ 48 | expect(body).to.match(/
2 | 7 | 8 | 9 | 10 | <%= pem %> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% claimTypes.forEach(function(ct) {%> 20 | 24 | <%= ct.displayName %> 25 | <%= ct.description %> 26 | 27 | <% }); %> 28 | 29 | 30 | <% if (mexEndpoint) { %> 31 | 32 | 33 |
https://test-adfs.auth0.com/adfs/services/trust/2005/certificatemixed
34 | 35 | 36 | 37 | 38 |
<%= mexEndpoint %>
39 |
40 |
41 |
42 |
43 |
44 |
45 | <% } %> 46 | 47 | 48 |
<%= endpoint %>
49 |
50 |
51 | -------------------------------------------------------------------------------- /test/xmlhelper.js: -------------------------------------------------------------------------------- 1 | var xmlCrypto = require('xml-crypto'), 2 | xmldom = require('xmldom'); 3 | 4 | exports.verifySignature = function(assertion, cert) { 5 | var doc = new xmldom.DOMParser().parseFromString(assertion); 6 | var signature = xmlCrypto.xpath.SelectNodes(doc, "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; 7 | var sig = new xmlCrypto.SignedXml(null, { idAttribute: 'AssertionID' }); 8 | sig.keyInfoProvider = { 9 | getKeyInfo: function (key) { 10 | return ""; 11 | }, 12 | getKey: function (keyInfo) { 13 | return cert; 14 | } 15 | }; 16 | sig.loadSignature(signature.toString()); 17 | return sig.checkSignature(assertion); 18 | }; 19 | 20 | exports.getIssuer = function(assertion) { 21 | var doc = new xmldom.DOMParser().parseFromString(assertion); 22 | return doc.documentElement.getAttribute('Issuer'); 23 | }; 24 | 25 | exports.getSignatureMethodAlgorithm = function(assertion) { 26 | var doc = new xmldom.DOMParser().parseFromString(assertion); 27 | return doc.documentElement 28 | .getElementsByTagName('SignatureMethod')[0] 29 | .getAttribute('Algorithm'); 30 | }; 31 | 32 | exports.getDigestMethodAlgorithm = function(assertion) { 33 | var doc = new xmldom.DOMParser().parseFromString(assertion); 34 | return doc.documentElement 35 | .getElementsByTagName('DigestMethod')[0] 36 | .getAttribute('Algorithm'); 37 | }; 38 | 39 | exports.getIssueInstant = function(assertion) { 40 | var doc = new xmldom.DOMParser().parseFromString(assertion); 41 | return doc.documentElement.getAttribute('IssueInstant'); 42 | }; 43 | 44 | exports.getConditions = function(assertion) { 45 | var doc = new xmldom.DOMParser().parseFromString(assertion); 46 | return doc.documentElement.getElementsByTagName('saml:Conditions'); 47 | }; 48 | 49 | exports.getAudiences = function(assertion) { 50 | var doc = new xmldom.DOMParser().parseFromString(assertion); 51 | return doc.documentElement 52 | .getElementsByTagName('saml:Conditions')[0] 53 | .getElementsByTagName('saml:AudienceRestrictionCondition')[0] 54 | .getElementsByTagName('saml:Audience'); 55 | }; 56 | 57 | exports.getAttributes = function(assertion) { 58 | var doc = new xmldom.DOMParser().parseFromString(assertion); 59 | return doc.documentElement 60 | .getElementsByTagName('saml:Attribute'); 61 | }; 62 | 63 | exports.getNameIdentifier = function(assertion) { 64 | var doc = new xmldom.DOMParser().parseFromString(assertion); 65 | return doc.documentElement 66 | .getElementsByTagName('saml:NameIdentifier')[0]; 67 | }; -------------------------------------------------------------------------------- /templates/federationServerService.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/federationServerService.tests.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var server = require('./fixture/server'); 3 | var request = require('request'); 4 | var xmldom = require('xmldom'); 5 | 6 | describe('wsfed federationserverservice', function () { 7 | before(function (done) { 8 | server.start(done); 9 | }); 10 | 11 | after(function (done) { 12 | server.close(done); 13 | }); 14 | 15 | var doc; 16 | 17 | before(function (done) { 18 | request.get({ 19 | jar: request.jar(), 20 | uri: 'http://localhost:5050/wsfed/adfs/fs/federationserverservice.asmx' 21 | }, function (err, response, b){ 22 | if(err) return done(err); 23 | doc = new xmldom.DOMParser().parseFromString(b).documentElement; 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should have the location field', function () { 29 | var location = doc.getElementsByTagName('soap:address')[0] 30 | .getAttribute('location'); 31 | expect(location) 32 | .to.equal('http://localhost:5050/wsfed/adfs/fs/federationserverservice.asmx'); 33 | }); 34 | 35 | it('should have the wsdl url', function () { 36 | var location = doc.getElementsByTagName('wsdl:import')[0] 37 | .getAttribute('location'); 38 | expect(location) 39 | .to.equal('http://localhost:5050/wsfed/adfs/fs/federationserverservice.asmx?wsdl=wsdl0'); 40 | }); 41 | 42 | describe('when loading wsdl', function () { 43 | var doc; 44 | 45 | before(function (done) { 46 | request.get({ 47 | jar: request.jar(), 48 | uri: 'http://localhost:5050/wsfed/adfs/fs/federationserverservice.asmx?wsdl=wsdl0' 49 | }, function (err, response, b){ 50 | if(err) return done(err); 51 | doc = new xmldom.DOMParser().parseFromString(b).documentElement; 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should have have portType', function(){ 57 | var portType = doc.getElementsByTagName('wsdl:portType')[0] 58 | .getAttribute('name'); 59 | 60 | expect(portType) 61 | .to.equal('ITrustInformationContract'); 62 | }); 63 | }); 64 | 65 | describe('when posting to the thumbprint endpoint', function () { 66 | var doc; 67 | 68 | before(function (done) { 69 | request.post({ 70 | jar: request.jar(), 71 | uri: 'http://localhost:5050/wsfed/adfs/fs/federationserverservice.asmx' 72 | }, function (err, response, b){ 73 | if(err) return done(err); 74 | //not sure how to test this yet... 75 | doc = new xmldom.DOMParser().parseFromString(b).documentElement; 76 | done(); 77 | }); 78 | }); 79 | 80 | it('should have have portType', function(){ 81 | // var portType = doc.getElementsByTagName('wsdl:portType')[0] 82 | // .getAttribute('name'); 83 | 84 | // expect(portType) 85 | // .to.equal('ITrustInformationContract'); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /lib/claims/PassportProfileMapper.js: -------------------------------------------------------------------------------- 1 | //shorthands claims namespaces 2 | var fm = { 3 | 'nameIdentifier': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier', 4 | 'email': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', 5 | 'name': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', 6 | 'givenname': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname', 7 | 'surname': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname', 8 | 'upn': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn', 9 | 'groups': 'http://schemas.xmlsoap.org/claims/Group' 10 | }; 11 | 12 | /** 13 | * 14 | * Passport User Profile Mapper 15 | * 16 | * A class to map passport.js user profile to a wsfed claims based identity. 17 | * 18 | * Passport Profile: 19 | * http://passportjs.org/guide/profile/ 20 | * 21 | * Claim Types: 22 | * http://msdn.microsoft.com/en-us/library/microsoft.identitymodel.claims.claimtypes_members.aspx 23 | * 24 | * @param {Object} pu Passport.js user profile 25 | */ 26 | function PassportProfileMapper (pu) { 27 | if(!(this instanceof PassportProfileMapper)) { 28 | return new PassportProfileMapper(pu); 29 | } 30 | this._pu = pu; 31 | } 32 | 33 | /** 34 | * map passport.js user profile to a wsfed claims based identity. 35 | * 36 | * @return {Object} WsFederation claim identity 37 | */ 38 | PassportProfileMapper.prototype.getClaims = function () { 39 | var claims = {}; 40 | 41 | claims[fm.nameIdentifier] = this._pu.id; 42 | 43 | if(Array.isArray(this._pu.emails) && this._pu.emails[0]) { 44 | claims[fm.email] = this._pu.emails[0].value; 45 | } 46 | 47 | claims[fm.name] = this._pu.displayName; 48 | 49 | if (this._pu.name) { 50 | claims[fm.givenname] = this._pu.name.givenName; 51 | claims[fm.surname] = this._pu.name.familyName; 52 | } 53 | 54 | var dontRemapAttributes = ['emails', 'displayName', 'name', 'id', '_json']; 55 | 56 | Object.keys(this._pu).filter(function (k) { 57 | return !~dontRemapAttributes.indexOf(k); 58 | }).forEach(function (k) { 59 | claims['http://schemas.passportjs.com/' + k] = this._pu[k]; 60 | }.bind(this)); 61 | return claims; 62 | }; 63 | 64 | /** 65 | * returns the nameidentifier for the saml token. 66 | * 67 | * @return {Object} object containing a nameIdentifier property and optional nameIdentifierFormat. 68 | */ 69 | PassportProfileMapper.prototype.getNameIdentifier = function () { 70 | var claims = this.getClaims(); 71 | 72 | return { 73 | nameIdentifier: claims[fm.nameIdentifier] || 74 | claims[fm.name] || 75 | claims[fm.emailaddress] 76 | }; 77 | 78 | }; 79 | 80 | /** 81 | * claims metadata used in the metadata endpoint. 82 | * 83 | * @param {Object} pu Passport.js profile 84 | * @return {[type]} WsFederation claim identity 85 | */ 86 | PassportProfileMapper.prototype.metadata = [ { 87 | id: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", 88 | optional: true, 89 | displayName: 'E-Mail Address', 90 | description: 'The e-mail address of the user' 91 | }, { 92 | id: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", 93 | optional: true, 94 | displayName: 'Given Name', 95 | description: 'The given name of the user' 96 | }, { 97 | id: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", 98 | optional: true, 99 | displayName: 'Name', 100 | description: 'The unique name of the user' 101 | }, { 102 | id: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", 103 | optional: true, 104 | displayName: 'Surname', 105 | description: 'The surname of the user' 106 | }, { 107 | id: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", 108 | optional: true, 109 | displayName: 'Name ID', 110 | description: 'The SAML name identifier of the user' 111 | }]; 112 | 113 | module.exports = PassportProfileMapper; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WS Federation middleware for node.js. 2 | 3 | [![Build Status](https://travis-ci.org/auth0/node-wsfed.png)](https://travis-ci.org/auth0/node-wsfed) 4 | 5 | ## Installation 6 | 7 | npm install wsfed 8 | 9 | ## Introduction 10 | 11 | This middleware is meant to generate a valid WSFederation endpoint that talks saml. 12 | 13 | The idea is that you will use another mechanism to valida the user first. 14 | 15 | The endpoint supports metadata as well in the url ```/FederationMetadata/2007-06/FederationMetadata.xml```. 16 | 17 | ## Usage 18 | 19 | Options 20 | 21 | | Name | Description | Default | 22 | | --------------------|:-------------------------------------------------| ---------------------------------------------| 23 | | cert | public key used by this identity provider | REQUIRED | 24 | | key | private key used by this identity provider | REQUIRED | 25 | | getPostURL | get the url to post the token f(wtrealm, wreply, req, callback) | REQUIRED | 26 | | issuer | the name of the issuer of the token | REQUIRED | 27 | | audience | the audience for the saml token | req.query.wtrealm || req.query.wreply | 28 | | getUserFromRequest | how to extract the user information from request | function(req) { return req.user; } | 29 | | profileMapper | mapper to map users to claims (see PassportProfileMapper)| PassportProfileMapper | 30 | | signatureAlgorithm | signature algorithm, options: rsa-sha1, rsa-sha256 | ```'rsa-sha256'``` | 31 | | digestAlgorithm | digest algorithm, options: sha1, sha256 | ```'sha256'``` | 32 | | wctx | state of the auth process | ```req.query.wctx``` | 33 | 34 | 35 | Add the middleware as follows: 36 | 37 | ~~~javascript 38 | app.get('/wsfed', wsfed.auth({ 39 | issuer: 'the-issuer', 40 | cert: fs.readFileSync(path.join(__dirname, 'some-cert.pem')), 41 | key: fs.readFileSync(path.join(__dirname, 'some-cert.key')), 42 | getPostUrl: function (wtrealm, wreply, req, callback) { 43 | return cb( null, 'http://someurl.com') 44 | } 45 | })); 46 | ~~~~ 47 | 48 | ## WsFederation Metadata 49 | 50 | wsfed can generate the metadata document for wsfederation as well. Usage as follows: 51 | 52 | ~~~javascript 53 | app.get('/wsfed/FederationMetadata/2007-06/FederationMetadata.xml', wsfed.metadata({ 54 | issuer: 'the-issuer', 55 | cert: fs.readFileSync(path.join(__dirname, 'some-cert.pem')), 56 | })); 57 | ~~~ 58 | 59 | It also accept two optionals parameters: 60 | 61 | - profileMapper: a class implementing the profile mapper. This is used to render the claims type information (using the metadata property). See [PassportProfileMapper](https://github.com/auth0/node-wsfed/blob/master/lib/claims/PassportProfileMapper.js) for more information. 62 | - endpointPath: this is the full path in your server to the auth route. By default the metadata handler uses the metadata request route without ```/FederationMetadata/2007..blabla.``` 63 | 64 | ## WsFederation Metadata endpoints ADFS1-like 65 | 66 | ADFS v1 uses another set of endpoints for the metadata and the thumbprint. If you have to connect an ADFS v1 client you have to do something like this: 67 | 68 | ~~~javascript 69 | app.get('/wsfed/adfs/fs/federationserverservice.asmx', 70 | wsfed.federationServerService.wsdl); 71 | 72 | app.post('/wsfed/adfs/fs/federationserverservice.asmx', 73 | wsfed.federationServerService.thumbprint({ 74 | pkcs7: yourPkcs7, 75 | cert: yourCert 76 | })); 77 | ~~~ 78 | 79 | notice that you need a ```pkcs7``` with the full chain of all certificates. You can generate this with openssl as follows: 80 | 81 | ~~~bash 82 | openssl crl2pkcs7 -nocrl \ 83 | -certfile your.crt \ 84 | -certfile another-cert-in-the-chain.crt \ 85 | -out contoso1.p7b 86 | ~~~ 87 | 88 | ## JWT 89 | 90 | By default the signed assertion is a SAML token, you can use JWT tokens as follows: 91 | 92 | ~~~javascript 93 | app.get('/wsfed', wsfed.auth({ 94 | jwt: true, 95 | issuer: 'the-issuer', 96 | key: fs.readFileSync(path.join(__dirname, 'some-cert.key')), 97 | getPostUrl: function (wtrealm, wreply, req, callback) { 98 | return cb( null, 'http://someurl.com') 99 | } 100 | })); 101 | ~~~~ 102 | 103 | ## License 104 | 105 | MIT - AUTH0 2013! -------------------------------------------------------------------------------- /lib/wsfed.js: -------------------------------------------------------------------------------- 1 | var templates = require('./templates'); 2 | var PassportProfileMapper = require('./claims/PassportProfileMapper'); 3 | var utils = require('./utils'); 4 | var saml11 = require('saml').Saml11; 5 | var jwt = require('jsonwebtoken'); 6 | var interpolate = require('./interpolate'); 7 | 8 | function asResource(res) { 9 | if(res.substr(0, 6) !== 'http:/' && 10 | res.substr(0, 6) !== 'https:' && 11 | res.substr(0, 4) !== 'urn:') { 12 | return 'urn:' + res; 13 | } 14 | return res; 15 | } 16 | 17 | /** 18 | * WSFederation middleware. 19 | * 20 | * This middleware creates a WSFed endpoint based on the user logged in identity. 21 | * 22 | * options: 23 | * - profileMapper(profile) a ProfileMapper implementation to convert a user profile to claims (PassportProfile). 24 | * - getUserFromRequest(req) a function that given a request returns the user. By default req.user 25 | * - issuer string 26 | * - cert the public certificate 27 | * - key the private certificate to sign all tokens 28 | * - postUrl function (wtrealm, wreply, request, callback) 29 | * 30 | * @param {[type]} options [description] 31 | * @return {[type]} [description] 32 | */ 33 | module.exports = function(options) { 34 | options = options || {}; 35 | options.profileMapper = options.profileMapper || PassportProfileMapper; 36 | options.getUserFromRequest = options.getUserFromRequest || function(req){ return req.user; }; 37 | 38 | if(typeof options.getPostURL !== 'function') { 39 | throw new Error('getPostURL is required'); 40 | } 41 | 42 | function renderResponse(res, postUrl, wctx, assertion) { 43 | res.set('Content-Type', 'text/html'); 44 | var model = { 45 | callback: postUrl, 46 | wctx: wctx, 47 | wresult: assertion 48 | }; 49 | var form; 50 | 51 | if (options.formTemplate) { 52 | form = interpolate(options.formTemplate); 53 | } else { 54 | form = templates[(!options.plain_form ? 'form' : 'form_el')]; 55 | } 56 | 57 | res.send(form(model)); 58 | } 59 | 60 | function execute (postUrl, req, res, next) { 61 | var audience = options.audience || 62 | req.query.wtrealm || 63 | req.query.wreply; 64 | 65 | if(!audience){ 66 | return next(new Error('audience is required')); 67 | } 68 | 69 | audience = asResource(audience); 70 | 71 | var user = options.getUserFromRequest(req); 72 | if(!user) return res.send(401); 73 | 74 | var ctx = options.wctx || req.query.wctx; 75 | if (!options.jwt) { 76 | var profileMap = options.profileMapper(user); 77 | var claims = profileMap.getClaims(options); 78 | var ni = profileMap.getNameIdentifier(options); 79 | saml11.create({ 80 | signatureAlgorithm: options.signatureAlgorithm, 81 | digestAlgorithm: options.digestAlgorithm, 82 | cert: options.cert, 83 | key: options.key, 84 | issuer: asResource(options.issuer), 85 | lifetimeInSeconds: options.lifetime || (60 * 60 * 8), 86 | audiences: audience, 87 | attributes: claims, 88 | nameIdentifier: ni.nameIdentifier, 89 | nameIdentifierFormat: ni.nameIdentifierFormat, 90 | encryptionPublicKey: options.encryptionPublicKey, 91 | encryptionCert: options.encryptionCert 92 | }, function(err, assertion) { 93 | if (err) return next(err); 94 | var escapedWctx = utils.escape(utils.escape(ctx)); // we need an escaped value for RequestSecurityTokenResponse.Context 95 | var escapedAssertion = utils.escape(assertion); // we need an escaped value for RequestSecurityTokenResponse.Context 96 | assertion = '' + escapedAssertion + ''; 97 | 98 | return renderResponse(res, postUrl, ctx, assertion); 99 | }); 100 | 101 | } else { 102 | var signed = jwt.sign(user, options.key.toString(), { 103 | expiresInMinutes: (options.lifetime || (60 * 60 * 8)) / 60, 104 | audience: audience, 105 | issuer: asResource(options.issuer), 106 | algorithm: options.jwtAlgorithm || 'RS256' 107 | }); 108 | 109 | return renderResponse(res, postUrl, ctx, signed); 110 | } 111 | } 112 | 113 | 114 | 115 | return function (req, res, next) { 116 | options.getPostURL(req.query.wtrealm, req.query.wreply, req, function (err, postUrl) { 117 | if (err) return next(err); 118 | if (!postUrl) return res.send(400, 'postUrl is required'); 119 | execute(postUrl, req, res, next); 120 | }); 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /test/wsfed.tests.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var server = require('./fixture/server'); 3 | var request = require('request'); 4 | var cheerio = require('cheerio'); 5 | var xmlhelper = require('./xmlhelper'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | describe('wsfed', function () { 10 | before(function (done) { 11 | server.start(done); 12 | }); 13 | 14 | after(function (done) { 15 | server.close(done); 16 | }); 17 | 18 | describe('authorizing', function () { 19 | var body, $, signedAssertion, attributes; 20 | 21 | before(function (done) { 22 | request.get({ 23 | jar: request.jar(), 24 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:the-super-client-id' 25 | }, function (err, response, b){ 26 | if(err) return done(err); 27 | body = b; 28 | $ = cheerio.load(body); 29 | var wresult = $('input[name="wresult"]').attr('value'); 30 | signedAssertion = /(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 31 | attributes = xmlhelper.getAttributes(signedAssertion); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should contain a form in the result', function(){ 37 | expect(body).to.match(/(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 108 | 109 | expect(xmlhelper.getAudiences(signedAssertion)[0].textContent) 110 | .to.equal('urn:auth0:superclient'); 111 | 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('when the wctx has ampersand(&)', function (){ 118 | it('should return escaped Context value', function (done) { 119 | var wctx = encodeURIComponent('rm=0&id=passive&ru=%2f'); 120 | 121 | request.get({ 122 | jar: request.jar(), 123 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=' + wctx + '&wtrealm=urn:auth0:superclient' 124 | }, function (err, response, b){ 125 | if(err) return done(err); 126 | var body = b; 127 | var $ = cheerio.load(body); 128 | var wresult = $('input[name="wresult"]').attr('value'); 129 | 130 | expect(wresult.indexOf(' Context="rm=0&id=passive&ru=%2f" ')) 131 | .to.be.above(-1); 132 | 133 | done(); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('when attribute has ampersand(&)', function (){ 139 | it('should return escaped value', function (done) { 140 | server.fakeUser.attribute_with_ampersand = 'http://foo?foo&foo'; 141 | request.get({ 142 | jar: request.jar(), 143 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wtrealm=urn:auth0:superclient' 144 | }, function (err, response, b){ 145 | if(err) return done(err); 146 | var body = b; 147 | var $ = cheerio.load(body); 148 | var wresult = $('input[name="wresult"]').attr('value'); 149 | 150 | expect(wresult.indexOf('http://foo?foo&foo')) 151 | .to.be.above(-1); 152 | 153 | delete server.fakeUser.attribute_with_ampersand; 154 | done(); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('when using an invalid callback url', function () { 160 | it('should return error', function(done){ 161 | request.get({ 162 | jar: request.jar(), 163 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:auth0:superclient&wreply=http://google.comcomcom' 164 | }, function (err, response){ 165 | if(err) return done(err); 166 | expect(response.statusCode) 167 | .to.equal(400); 168 | done(); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | -------------------------------------------------------------------------------- /test/wsfed.custom_form.tests.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var server = require('./fixture/server'); 3 | var request = require('request'); 4 | var cheerio = require('cheerio'); 5 | var xmlhelper = require('./xmlhelper'); 6 | var fs = require('fs'); 7 | 8 | describe('wsfed', function () { 9 | before(function (done) { 10 | server.start({ 11 | formTemplate: fs.readFileSync(__dirname + '/custom_form.html').toString() 12 | }, done); 13 | }); 14 | 15 | after(function (done) { 16 | server.close(done); 17 | }); 18 | 19 | describe('authorizing', function () { 20 | var body, $, signedAssertion, attributes; 21 | 22 | before(function (done) { 23 | request.get({ 24 | jar: request.jar(), 25 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:the-super-client-id' 26 | }, function (err, response, b){ 27 | if(err) return done(err); 28 | body = b; 29 | $ = cheerio.load(body); 30 | var wresult = $('input[name="wresult"]').attr('value'); 31 | signedAssertion = /(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 32 | attributes = xmlhelper.getAttributes(signedAssertion); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should contain a form in the result', function(){ 38 | expect(body).to.match(/(.*)<\/t:RequestedSecurityToken>/.exec(wresult)[1]; 109 | 110 | expect(xmlhelper.getAudiences(signedAssertion)[0].textContent) 111 | .to.equal('urn:auth0:superclient'); 112 | 113 | done(); 114 | }); 115 | }); 116 | }); 117 | 118 | describe('when the wctx has ampersand(&)', function (){ 119 | it('should return escaped Context value', function (done) { 120 | var wctx = encodeURIComponent('rm=0&id=passive&ru=%2f'); 121 | 122 | request.get({ 123 | jar: request.jar(), 124 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=' + wctx + '&wtrealm=urn:auth0:superclient' 125 | }, function (err, response, b){ 126 | if(err) return done(err); 127 | var body = b; 128 | var $ = cheerio.load(body); 129 | var wresult = $('input[name="wresult"]').attr('value'); 130 | 131 | expect(wresult.indexOf(' Context="rm=0&id=passive&ru=%2f" ')) 132 | .to.be.above(-1); 133 | 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('when attribute has ampersand(&)', function (){ 140 | it('should return escaped value', function (done) { 141 | server.fakeUser.attribute_with_ampersand = 'http://foo?foo&foo'; 142 | request.get({ 143 | jar: request.jar(), 144 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wtrealm=urn:auth0:superclient' 145 | }, function (err, response, b){ 146 | if(err) return done(err); 147 | var body = b; 148 | var $ = cheerio.load(body); 149 | var wresult = $('input[name="wresult"]').attr('value'); 150 | 151 | expect(wresult.indexOf('http://foo?foo&foo')) 152 | .to.be.above(-1); 153 | 154 | delete server.fakeUser.attribute_with_ampersand; 155 | done(); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('when using an invalid callback url', function () { 161 | it('should return error', function(done){ 162 | request.get({ 163 | jar: request.jar(), 164 | uri: 'http://localhost:5050/wsfed?wa=wsignin1.0&wctx=123&wtrealm=urn:auth0:superclient&wreply=http://google.comcomcom' 165 | }, function (err, response){ 166 | if(err) return done(err); 167 | expect(response.statusCode) 168 | .to.equal(400); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | 175 | --------------------------------------------------------------------------------