From 2a73b1a4314fffbb7d5b469c3a3e7a087d490708 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 25 Mar 2017 15:17:21 +0100 Subject: [PATCH] Add the access_control entry in the config file to allow the user to define per group rules to access the subdomains --- config.template.yml | 45 ++++- docker-compose.yml | 3 + example/ldap/base.ldif | 70 ++++--- example/nginx_conf/nginx.conf | 2 +- src/index.js | 19 +- src/lib/config_adapter.js | 7 +- src/lib/ldap.js | 150 ++++++++++---- src/lib/routes/first_factor.js | 62 ++++-- src/lib/routes/reset_password.js | 19 +- src/lib/routes/verify.js | 20 ++ src/lib/server.js | 15 +- test/unitary/routes/test_first_factor.js | 91 +++++---- test/unitary/routes/test_reset_password.js | 35 ++-- test/unitary/routes/test_verify.js | 64 +++++- test/unitary/test_config_adapter.js | 19 +- test/unitary/test_data_persistence.js | 21 +- test/unitary/test_ldap.js | 217 +++++++++++++-------- test/unitary/test_server.js | 42 ++-- test/unitary/test_server_config.js | 9 +- 19 files changed, 620 insertions(+), 290 deletions(-) diff --git a/config.template.yml b/config.template.yml index 1f6a20e59..b84fb053e 100644 --- a/config.template.yml +++ b/config.template.yml @@ -1,20 +1,57 @@ # The port to listen on -port: 8080 +port: 80 +# Log level +# # Level of verbosity for logs logs_level: info -# Configuration of LDAP +# LDAP configuration +# # Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com ldap: + # The url of the ldap server url: ldap://ldap - user_search_base: ou=users,dc=example,dc=com - user_search_filter: cn + + # The base dn for every entries + base_dn: dc=example,dc=com + + # An additional dn to define the scope to all users + additional_user_dn: ou=users + + # The user name attribute of users. Might uid for FreeIPA. 'cn' by default. + user_name_attribute: cn + + # An additional dn to define the scope of groups + additional_group_dn: ou=groups + + # The group name attribute of group. 'cn' by default. + group_name_attribute: cn + + # The username and password of the admin user. user: cn=admin,dc=example,dc=com password: password +# Access Control +# +# Access control is a set of rules where you can specify a group-based +# subdomain restrictions. +# +# If access_control is not defined, ACL rules are disabled and default policy +# is allowed to everyone. +# Otherwise, the default policy is denied for any user and any subdomain. +access_control: + - group: admin + allowed_domains: + - secret.test.local + - secret1.test.local + - group: dev + allowed_domains: + - secret2.test.local + + # Configuration of session cookies # # _secret_ the secret to encrypt session cookies diff --git a/docker-compose.yml b/docker-compose.yml index bf30b905e..bfaaeb331 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,9 @@ services: - SLAPD_ORGANISATION=MyCompany - SLAPD_DOMAIN=example.com - SLAPD_PASSWORD=password + - SLAPD_ADDITIONAL_MODULES=memberof + - SLAPD_ADDITIONAL_SCHEMAS=openldap + - SLAPD_FORCE_RECONFIGURE=true expose: - "389" volumes: diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif index 1e46c0ef7..07d4e5a89 100644 --- a/example/ldap/base.ldif +++ b/example/ldap/base.ldif @@ -8,39 +8,55 @@ objectclass: organizationalUnit objectclass: top ou: users -dn: cn=user,ou=groups,dc=example,dc=com -cn: user -gidnumber: 502 -objectclass: posixGroup +dn: cn=dev,ou=groups,dc=example,dc=com +cn: dev +member: cn=john,ou=users,dc=example,dc=com +member: cn=bob,ou=users,dc=example,dc=com +objectclass: groupOfNames objectclass: top -dn: cn=user,ou=users,dc=example,dc=com -cn: user -gidnumber: 500 -givenname: user -homedirectory: /home/user1 -loginshell: /bin/sh -objectclass: inetOrgPerson -objectclass: posixAccount +dn: cn=admin,ou=groups,dc=example,dc=com +cn: admin +member: cn=john,ou=users,dc=example,dc=com +objectclass: groupOfNames objectclass: top -mail: user@example.com -sn: User -uid: user -uidnumber: 1000 + +dn: cn=john,ou=users,dc=example,dc=com +cn: john +objectclass: inetOrgPerson +objectclass: top +mail: john.doe@example.com +sn: John Doe userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= -dn: uid=useruid,ou=users,dc=example,dc=com -cn: useruid -gidnumber: 500 -givenname: user -homedirectory: /home/user1 -loginshell: /bin/sh +dn: cn=harry,ou=users,dc=example,dc=com +cn: harry objectclass: inetOrgPerson -objectclass: posixAccount objectclass: top -mail: useruid@example.com -sn: User -uid: useruid -uidnumber: 1001 +mail: harry.potter@example.com +sn: Harry Potter userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= +dn: cn=bob,ou=users,dc=example,dc=com +cn: bob +objectclass: inetOrgPerson +objectclass: top +mail: bob.dylan@example.com +sn: Bob Dylan +userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= + +# dn: uid=jack,ou=users,dc=example,dc=com +# cn: jack +# gidnumber: 501 +# givenname: Jack +# homedirectory: /home/jack +# loginshell: /bin/sh +# objectclass: inetOrgPerson +# objectclass: posixAccount +# objectclass: top +# mail: jack.daniels@example.com +# sn: Jack Daniels +# uid: jack +# uidnumber: 1001 +# userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= +# diff --git a/example/nginx_conf/nginx.conf b/example/nginx_conf/nginx.conf index 5ffafad61..cfb8c62e5 100644 --- a/example/nginx_conf/nginx.conf +++ b/example/nginx_conf/nginx.conf @@ -73,8 +73,8 @@ http { location /authentication/verify { proxy_set_header X-Original-URI $request_uri; - proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; proxy_pass http://auth/authentication/verify; } diff --git a/src/index.js b/src/index.js index d50549a7b..e593e48b8 100755 --- a/src/index.js +++ b/src/index.js @@ -4,12 +4,14 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; var server = require('./lib/server'); -var ldap = require('ldapjs'); +var ldapjs = require('ldapjs'); var u2f = require('authdog'); var nodemailer = require('nodemailer'); var nedb = require('nedb'); var YAML = require('yamljs'); var session = require('express-session'); +var winston = require('winston'); +var speakeasy = require('speakeasy'); var config_path = process.argv[2]; if(!config_path) { @@ -22,20 +24,13 @@ console.log('Parse configuration file: %s', config_path); var yaml_config = YAML.load(config_path); -var ldap_client = ldap.createClient({ - url: config.ldap_url, - reconnect: true -}); - -ldap_client.on('error', function(err) { - console.error('LDAP Error:', err.message) -}) - var deps = {}; deps.u2f = u2f; deps.nedb = nedb; deps.nodemailer = nodemailer; -deps.ldap = ldap; +deps.ldapjs = ldapjs; deps.session = session; +deps.winston = winston; +deps.speakeasy = speakeasy; -server.run(yaml_config, ldap_client, deps); +server.run(yaml_config, deps); diff --git a/src/lib/config_adapter.js b/src/lib/config_adapter.js index 0fa486740..e91547f2b 100644 --- a/src/lib/config_adapter.js +++ b/src/lib/config_adapter.js @@ -4,17 +4,14 @@ var objectPath = require('object-path'); module.exports = function(yaml_config) { return { port: objectPath.get(yaml_config, 'port', 8080), - ldap_url: objectPath.get(yaml_config, 'ldap.url', 'ldap://127.0.0.1:389'), - ldap_user_search_base: objectPath.get(yaml_config, 'ldap.user_search_base'), - ldap_user_search_filter: objectPath.get(yaml_config, 'ldap.user_search_filter'), - ldap_user: objectPath.get(yaml_config, 'ldap.user'), - ldap_password: objectPath.get(yaml_config, 'ldap.password'), + ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'), session_domain: objectPath.get(yaml_config, 'session.domain'), session_secret: objectPath.get(yaml_config, 'session.secret'), session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms store_directory: objectPath.get(yaml_config, 'store_directory'), logs_level: objectPath.get(yaml_config, 'logs_level'), notifier: objectPath.get(yaml_config, 'notifier'), + access_control: objectPath.get(yaml_config, 'access_control') } }; diff --git a/src/lib/ldap.js b/src/lib/ldap.js index 01c5289e1..0007dffcd 100644 --- a/src/lib/ldap.js +++ b/src/lib/ldap.js @@ -1,46 +1,66 @@ -module.exports = { - validate: validate_credentials, - get_email: retrieve_email, - update_password: update_password -} +module.exports = Ldap; var util = require('util'); var Promise = require('bluebird'); var exceptions = require('./exceptions'); var Dovehash = require('dovehash'); -function validate_credentials(ldap_client, username, password, user_base, user_filter) { - // if not provided, default to cn - if(!user_filter) user_filter = 'cn'; +function Ldap(deps, ldap_config) { + this.ldap_config = ldap_config; - var userDN = util.format("%s=%s,%s", user_filter, username, user_base); - console.log(userDN); - var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client }); - return bind_promised(userDN, password) + this.ldapjs = deps.ldapjs; + this.logger = deps.winston; + + this.connect(); +} + +Ldap.prototype.connect = function() { + var ldap_client = this.ldapjs.createClient({ + url: this.ldap_config.url, + reconnect: true + }); + + ldap_client.on('error', function(err) { + console.error('LDAP Error:', err.message) + }); + + this.ldap_client = Promise.promisifyAll(ldap_client); +} + +Ldap.prototype._build_user_dn = function(username) { + var user_name_attr = this.ldap_config.user_name_attribute; + // if not provided, default to cn + if(!user_name_attr) user_name_attr = 'cn'; + + var additional_user_dn = this.ldap_config.additional_user_dn; + var base_dn = this.ldap_config.base_dn; + + var user_dn = util.format("%s=%s", user_name_attr, username); + if(additional_user_dn) user_dn += util.format(",%s", additional_user_dn); + user_dn += util.format(',%s', base_dn); + return user_dn; +} + +Ldap.prototype.bind = function(username, password) { + var user_dn = this._build_user_dn(username); + + this.logger.debug('LDAP: Bind user %s', user_dn); + return this.ldap_client.bindAsync(user_dn, password) .error(function(err) { - console.error(err); throw new exceptions.LdapBindError(err.message); }); } -function retrieve_email(ldap_client, username, user_base, user_filter) { - // if not provided, default to cn - if(!user_filter) user_filter = 'cn'; - - var userDN = util.format("%s=%s,%s", user_filter, username, user_base); - console.log(userDN); - var search_promised = Promise.promisify(ldap_client.search, { context: ldap_client }); - var query = {}; - query.sizeLimit = 1; - query.attributes = ['mail']; - +Ldap.prototype._search_in_ldap = function(base, query) { + var that = this; + this.logger.debug('LDAP: Search for %s in %s', JSON.stringify(query), base); return new Promise(function(resolve, reject) { - search_promised(userDN, query) + that.ldap_client.searchAsync(base, query) .then(function(res) { - var doc; + var doc = []; res.on('searchEntry', function(entry) { - doc = entry.object; + doc.push(entry.object); }); res.on('error', function(err) { reject(new exceptions.LdapSearchError(err)); @@ -55,26 +75,80 @@ function retrieve_email(ldap_client, username, user_base, user_filter) { }); } -function update_password(ldap_client, ldap, username, new_password, config) { - var user_filter = config.ldap_user_search_filter; - // if not provided, default to cn - if(!user_filter) user_filter = 'cn'; +Ldap.prototype.get_groups = function(username) { + var user_dn = this._build_user_dn(username); + + var group_name_attr = this.ldap_config.group_name_attribute; + if(!group_name_attr) group_name_attr = 'cn'; + + var additional_group_dn = this.ldap_config.additional_group_dn; + var base_dn = this.ldap_config.base_dn; + + var group_dn = base_dn; + if(additional_group_dn) + group_dn = util.format('%s,', additional_group_dn) + group_dn; + + var query = {}; + query.scope = 'sub'; + query.attributes = [group_name_attr]; + query.filter = 'member=' + user_dn ; + + var that = this; + this.logger.debug('LDAP: get groups of user %s', username); + return this._search_in_ldap(group_dn, query) + .then(function(docs) { + var groups = []; + for(var i = 0; i= 0) { + var domains = rule.allowed_domains; + allowed_domains = allowed_domains.concat(domains); + } + } + } + return allowed_domains; +} function first_factor(req, res) { - var logger = req.app.get('logger'); var username = req.body.username; var password = req.body.password; if(!username || !password) { @@ -15,59 +29,69 @@ function first_factor(req, res) { return; } - logger.info('1st factor: Starting authentication of user "%s"', username); - - var ldap_client = req.app.get('ldap client'); + var logger = req.app.get('logger'); + var ldap = req.app.get('ldap'); var config = req.app.get('config'); var regulator = req.app.get('authentication regulator'); + logger.info('1st factor: Starting authentication of user "%s"', username); logger.debug('1st factor: Start bind operation against LDAP'); logger.debug('1st factor: username=%s', username); - logger.debug('1st factor: base_dn=%s', config.ldap_user_search_base); - logger.debug('1st factor: user_filter=%s', config.ldap_user_search_filter); regulator.regulate(username) .then(function() { - return ldap.validate(ldap_client, username, password, config.ldap_user_search_base, config.ldap_user_search_filter); + return ldap.bind(username, password); }) .then(function() { objectPath.set(req, 'session.auth_session.userid', username); objectPath.set(req, 'session.auth_session.first_factor', true); logger.info('1st factor: LDAP binding successful'); logger.debug('1st factor: Retrieve email from LDAP'); - return ldap.get_email(ldap_client, username, config.ldap_user_search_base, - config.ldap_user_search_filter) + return Promise.join(ldap.get_emails(username), ldap.get_groups(username)); }) - .then(function(doc) { - var email = objectPath.get(doc, 'mail'); - logger.debug('1st factor: document=%s', JSON.stringify(doc)); - logger.debug('1st factor: Retrieved email is %s', email); + .then(function(data) { + var emails = data[0]; + var groups = data[1]; + + if(!emails && emails.length <= 0) throw new Error('No email found'); + logger.debug('1st factor: Retrieved email are %s', emails); + objectPath.set(req, 'session.auth_session.email', emails[0]); + + if(config.access_control) { + var allowed_domains = get_allowed_domains(config.access_control, groups); + logger.debug('1st factor: allowed domains are %s', allowed_domains); + objectPath.set(req, 'session.auth_session.allowed_domains', + allowed_domains); + } + else { + logger.debug('1st factor: no access control rules found.' + + 'Default policy to allow all.'); + } - objectPath.set(req, 'session.auth_session.email', email); regulator.mark(username, true); res.status(204); res.send(); }) .catch(exceptions.LdapSearchError, function(err) { - logger.info('1st factor: Unable to retrieve email from LDAP', err); + logger.error('1st factor: Unable to retrieve email from LDAP', err); res.status(500); res.send(); }) .catch(exceptions.LdapBindError, function(err) { - logger.info('1st factor: LDAP binding failed'); + logger.error('1st factor: LDAP binding failed'); logger.debug('1st factor: LDAP binding failed due to ', err); regulator.mark(username, false); res.status(401); res.send('Bad credentials'); }) .catch(exceptions.AuthenticationRegulationError, function(err) { - logger.info('1st factor: the regulator rejected the authentication of user %s', username); + logger.error('1st factor: the regulator rejected the authentication of user %s', username); logger.debug('1st factor: authentication rejected due to %s', err); res.status(403); res.send('Access has been restricted for a few minutes...'); }) .catch(function(err) { - logger.debug('1st factor: Unhandled error %s', err); + logger.error('1st factor: Unhandled error %s', err); res.status(500); res.send('Internal error'); }); diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js index 8b46cf159..dd26bf5c9 100644 --- a/src/lib/routes/reset_password.js +++ b/src/lib/routes/reset_password.js @@ -1,7 +1,6 @@ var Promise = require('bluebird'); var objectPath = require('object-path'); -var ldap = require('../ldap'); var exceptions = require('../exceptions'); var CHALLENGE = 'reset-password'; @@ -24,16 +23,14 @@ function pre_check(req) { return Promise.reject(err); } - var ldap_client = req.app.get('ldap client'); - var config = req.app.get('config'); + var ldap = req.app.get('ldap'); - return ldap.get_email(ldap_client, userid, config.ldap_user_search_base, - config.ldap_user_search_filter) - .then(function(doc) { - var email = objectPath.get(doc, 'mail'); + return ldap.get_emails(userid) + .then(function(emails) { + if(!emails && emails.length <= 0) throw new Error('No email found'); var identity = {} - identity.email = email; + identity.email = emails[0]; identity.userid = userid; return Promise.resolve(identity); }); @@ -53,15 +50,13 @@ function protect(fn) { function post(req, res) { var logger = req.app.get('logger'); - var ldapjs = req.app.get('ldap'); - var ldap_client = req.app.get('ldap client'); + var ldap = req.app.get('ldap'); var new_password = objectPath.get(req, 'body.password'); var userid = objectPath.get(req, 'session.auth_session.identity_check.userid'); - var config = req.app.get('config'); logger.info('POST reset-password: User %s wants to reset his/her password', userid); - ldap.update_password(ldap_client, ldapjs, userid, new_password, config) + ldap.update_password(userid, new_password) .then(function() { logger.info('POST reset-password: Password reset for user %s', userid); objectPath.set(req, 'session.auth_session', undefined); diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js index 063328274..e1be3764f 100644 --- a/src/lib/routes/verify.js +++ b/src/lib/routes/verify.js @@ -5,6 +5,8 @@ var objectPath = require('object-path'); var Promise = require('bluebird'); function verify_filter(req, res) { + var logger = req.app.get('logger'); + if(!objectPath.has(req, 'session.auth_session')) return Promise.reject('No auth_session variable'); @@ -14,6 +16,23 @@ function verify_filter(req, res) { if(!objectPath.has(req, 'session.auth_session.second_factor')) return Promise.reject('No second factor variable'); + if(!objectPath.has(req, 'session.auth_session.userid')) + return Promise.reject('No userid variable'); + + var config = req.app.get('config'); + var access_control = config.access_control; + + if(access_control) { + var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains'); + var host = objectPath.get(req, 'headers.host'); + var domain = host.split(':')[0]; + logger.debug('Trying to access domain: %s', domain); + logger.debug('User has access to %s', JSON.stringify(allowed_domains)); + + if(allowed_domains.indexOf(domain) < 0) + return Promise.reject('Access restricted by ACL rules'); + } + if(!req.session.auth_session.first_factor || !req.session.auth_session.second_factor) return Promise.reject('First or second factor not validated'); @@ -28,6 +47,7 @@ function verify(req, res) { res.send(); }) .catch(function(err) { + req.app.get('logger').error(err); res.status(401); res.send(); }); diff --git a/src/lib/server.js b/src/lib/server.js index 745ca93f8..1aa51b64d 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -5,16 +5,15 @@ module.exports = { var express = require('express'); var bodyParser = require('body-parser'); -var speakeasy = require('speakeasy'); var path = require('path'); -var winston = require('winston'); var UserDataStore = require('./user_data_store'); var Notifier = require('./notifier'); var AuthenticationRegulator = require('./authentication_regulator'); var setup_endpoints = require('./setup_endpoints'); var config_adapter = require('./config_adapter'); +var Ldap = require('./ldap'); -function run(yaml_config, ldap_client, deps, fn) { +function run(yaml_config, deps, fn) { var config = config_adapter(yaml_config); var view_directory = path.resolve(__dirname, '../views'); @@ -45,17 +44,17 @@ function run(yaml_config, ldap_client, deps, fn) { app.set('view engine', 'ejs'); // by default the level of logs is info - winston.level = config.logs_level || 'info'; + deps.winston.level = config.logs_level || 'info'; var five_minutes = 5 * 60; var data_store = new UserDataStore(deps.nedb, datastore_options); var regulator = new AuthenticationRegulator(data_store, five_minutes); var notifier = new Notifier(config.notifier, deps); + var ldap = new Ldap(deps, config.ldap); - app.set('logger', winston); - app.set('ldap', deps.ldap); - app.set('ldap client', ldap_client); - app.set('totp engine', speakeasy); + app.set('logger', deps.winston); + app.set('ldap', ldap); + app.set('totp engine', deps.speakeasy); app.set('u2f', deps.u2f); app.set('user data store', data_store); app.set('notifier', notifier); diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js index 548b6dd73..4798f9820 100644 --- a/test/unitary/routes/test_first_factor.js +++ b/test/unitary/routes/test_first_factor.js @@ -5,35 +5,28 @@ var assert = require('assert'); var winston = require('winston'); var first_factor = require('../../../src/lib/routes/first_factor'); var exceptions = require('../../../src/lib/exceptions'); +var Ldap = require('../../../src/lib/ldap'); describe('test the first factor validation route', function() { var req, res; var ldap_interface_mock; + var emails; var search_res_ok; var regulator; + var config; beforeEach(function() { - ldap_interface_mock = { - bind: sinon.stub(), - search: sinon.stub() - } - var config = { - ldap_user_search_base: 'ou=users,dc=example,dc=com', - ldap_user_search_filter: 'uid' - } - - var search_doc = { - object: { - mail: 'test_ok@example.com' + ldap_interface_mock = sinon.createStubInstance(Ldap); + config = { + ldap: { + base_dn: 'ou=users,dc=example,dc=com', + user_name_attribute: 'uid' } - }; - - var search_res_ok = {}; - search_res_ok.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(search_doc); - }); - ldap_interface_mock.search.yields(undefined, search_res_ok); + } + emails = [ 'test_ok@example.com' ]; + groups = [ 'group1', 'group2' ]; + regulator = {}; regulator.mark = sinon.stub(); regulator.regulate = sinon.stub(); @@ -42,7 +35,7 @@ describe('test the first factor validation route', function() { regulator.regulate.returns(Promise.resolve()); var app_get = sinon.stub(); - app_get.withArgs('ldap client').returns(ldap_interface_mock); + app_get.withArgs('ldap').returns(ldap_interface_mock); app_get.withArgs('config').returns(config); app_get.withArgs('logger').returns(winston); app_get.withArgs('authentication regulator').returns(regulator); @@ -75,43 +68,69 @@ describe('test the first factor validation route', function() { assert.equal(204, res.status.getCall(0).args[0]); resolve(); }); - ldap_interface_mock.bind.yields(undefined); + ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); + ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); first_factor(req, res); }); }); - it('should bind user based on LDAP DN', function(done) { - ldap_interface_mock.bind = sinon.spy(function(dn) { - if(dn == 'uid=username,ou=users,dc=example,dc=com') done(); + it('should store the allowed domains in the auth session', function() { + config.access_control = []; + config.access_control.push({ + group: 'group1', + allowed_domains: ['domain1.example.com', 'domain2.example.com'] + }); + return new Promise(function(resolve, reject) { + res.send = sinon.spy(function(data) { + assert.deepEqual(['domain1.example.com', 'domain2.example.com'], + req.session.auth_session.allowed_domains); + assert.equal(204, res.status.getCall(0).args[0]); + resolve(); + }); + ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); + ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); + ldap_interface_mock.get_groups.returns(Promise.resolve(groups)); + first_factor(req, res); }); - first_factor(req, res); }); it('should retrieve email from LDAP', function(done) { - ldap_interface_mock.bind.yields(undefined); - ldap_interface_mock.search = sinon.spy(function(dn) { - if(dn == 'uid=username,ou=users,dc=example,dc=com') done(); - }); + res.send = sinon.spy(function(data) { done(); }); + ldap_interface_mock.bind.returns(Promise.resolve()); + ldap_interface_mock.get_emails = sinon.stub().withArgs('usernam').returns(Promise.resolve([{mail: ['test@example.com'] }])); first_factor(req, res); }); - it('should return status code 401 when LDAP binding fails', function(done) { + it('should set email as session variables', function() { + return new Promise(function(resolve, reject) { + res.send = sinon.spy(function(data) { + assert.equal('test_ok@example.com', req.session.auth_session.email); + resolve(); + }); + var emails = [ 'test_ok@example.com' ]; + ldap_interface_mock.bind.returns(Promise.resolve()); + ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); + first_factor(req, res); + }); + }); + + it('should return status code 401 when LDAP binding throws', function(done) { res.send = sinon.spy(function(data) { assert.equal(401, res.status.getCall(0).args[0]); assert.equal(regulator.mark.getCall(0).args[0], 'username'); done(); }); - ldap_interface_mock.bind.yields('Bad credentials'); + ldap_interface_mock.bind.throws(new exceptions.LdapBindError('Bad credentials')); first_factor(req, res); }); - it('should return status code 500 when LDAP binding throws', function(done) { + it('should return status code 500 when LDAP search throws', function(done) { res.send = sinon.spy(function(data) { assert.equal(500, res.status.getCall(0).args[0]); done(); }); - ldap_interface_mock.bind.yields(undefined); - ldap_interface_mock.search.yields('error'); + ldap_interface_mock.bind.returns(Promise.resolve()); + ldap_interface_mock.get_emails.throws(new exceptions.LdapSearchError('err')); first_factor(req, res); }); @@ -122,8 +141,8 @@ describe('test the first factor validation route', function() { assert.equal(403, res.status.getCall(0).args[0]); done(); }); - ldap_interface_mock.bind.yields(undefined); - ldap_interface_mock.search.yields(undefined); + ldap_interface_mock.bind.returns(Promise.resolve()); + ldap_interface_mock.get_emails.returns(Promise.resolve()); first_factor(req, res); }); }); diff --git a/test/unitary/routes/test_reset_password.js b/test/unitary/routes/test_reset_password.js index 14bff5a32..efac684d4 100644 --- a/test/unitary/routes/test_reset_password.js +++ b/test/unitary/routes/test_reset_password.js @@ -1,6 +1,8 @@ +var reset_password = require('../../../src/lib/routes/reset_password'); +var Ldap = require('../../../src/lib/ldap'); + var sinon = require('sinon'); var winston = require('winston'); -var reset_password = require('../../../src/lib/routes/reset_password'); var assert = require('assert'); describe('test reset password', function() { @@ -35,20 +37,30 @@ describe('test reset password', function() { user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); req.app.get.withArgs('user data store').returns(user_data_store); - ldap = {}; - ldap.Change = sinon.spy(); - req.app.get.withArgs('ldap').returns(ldap); + + config = {}; + config.ldap = {}; + config.ldap.base_dn = 'dc=example,dc=com'; + config.ldap.user_name_attribute = 'cn'; + req.app.get.withArgs('config').returns(config); ldap_client = {}; ldap_client.bind = sinon.stub(); ldap_client.search = sinon.stub(); ldap_client.modify = sinon.stub(); - req.app.get.withArgs('ldap client').returns(ldap_client); + ldap_client.on = sinon.spy(); - config = {}; - config.ldap_user_search_base = 'dc=example,dc=com'; - config.ldap_user_search_filter = 'cn'; - req.app.get.withArgs('config').returns(config); + ldapjs = {}; + ldapjs.Change = sinon.spy(); + ldapjs.createClient = sinon.spy(function() { + return ldap_client; + }); + + deps = { + ldapjs: ldapjs, + winston: winston + }; + req.app.get.withArgs('ldap').returns(new Ldap(deps, config.ldap)); res = {}; res.send = sinon.spy(); @@ -77,9 +89,8 @@ describe('test reset password', function() { }); it('should perform a search in ldap to find email address', function(done) { - config.ldap_user_search_filter = 'uid'; + config.ldap.user_name_attribute = 'uid'; ldap_client.search = sinon.spy(function(dn) { - console.log(dn); if(dn == 'uid=user,dc=example,dc=com') done(); }); reset_password.icheck_interface.pre_check_callback(req); @@ -88,7 +99,7 @@ describe('test reset password', function() { it('should returns identity when ldap replies', function(done) { var doc = {}; doc.object = {}; - doc.object.email = 'test@example.com'; + doc.object.email = ['test@example.com']; doc.object.userid = 'user'; var res = {}; diff --git a/test/unitary/routes/test_verify.js b/test/unitary/routes/test_verify.js index 2e057a1d0..38c08bebc 100644 --- a/test/unitary/routes/test_verify.js +++ b/test/unitary/routes/test_verify.js @@ -2,19 +2,33 @@ var assert = require('assert'); var verify = require('../../../src/lib/routes/verify'); var sinon = require('sinon'); +var winston = require('winston'); describe('test authentication token verification', function() { var req, res; + var config_mock; beforeEach(function() { + config_mock = {}; req = {}; res = {}; + req.headers = {}; + req.headers.host = 'secret.example.com'; + req.app = {}; + req.app.get = sinon.stub(); + req.app.get.withArgs('config').returns(config_mock); + req.app.get.withArgs('logger').returns(winston); res.status = sinon.spy(); }); it('should be already authenticated', function(done) { req.session = {}; - req.session.auth_session = {first_factor: true, second_factor: true}; + req.session.auth_session = { + first_factor: true, + second_factor: true, + userid: 'myuser', + group: 'mygroup' + }; res.send = sinon.spy(function() { assert.equal(204, res.status.getCall(0).args[0]); @@ -25,13 +39,13 @@ describe('test authentication token verification', function() { }); describe('given different cases of session', function() { - function test_unauthorized(auth_session) { + function test_session(auth_session, status_code) { return new Promise(function(resolve, reject) { req.session = {}; req.session.auth_session = auth_session; res.send = sinon.spy(function() { - assert.equal(401, res.status.getCall(0).args[0]); + assert.equal(status_code, res.status.getCall(0).args[0]); resolve(); }); @@ -39,6 +53,14 @@ describe('test authentication token verification', function() { }); } + function test_unauthorized(auth_session) { + return test_session(auth_session, 401); + } + + function test_authorized(auth_session) { + return test_session(auth_session, 204); + } + it('should not be authenticated when second factor is missing', function() { return test_unauthorized({ first_factor: true, second_factor: false }); }); @@ -47,6 +69,14 @@ describe('test authentication token verification', function() { return test_unauthorized({ first_factor: false, second_factor: true }); }); + it('should not be authenticated when userid is missing', function() { + return test_unauthorized({ + first_factor: true, + second_factor: true, + group: 'mygroup', + }); + }); + it('should not be authenticated when first and second factor are missing', function() { return test_unauthorized({ first_factor: false, second_factor: false }); }); @@ -55,6 +85,34 @@ describe('test authentication token verification', function() { return test_unauthorized(undefined); }); + it('should reply unauthorized when the domain is restricted', function() { + config_mock.access_control = []; + config_mock.access_control.push({ + group: 'abc', + allowed_domains: ['secret.example.com'] + }); + return test_unauthorized({ + first_factor: true, + second_factor: true, + userid: 'user', + allowed_domains: ['restricted.example.com'] + }); + }); + + it('should reply authorized when the domain is allowed', function() { + config_mock.access_control = []; + config_mock.access_control.push({ + group: 'abc', + allowed_domains: ['secret.example.com'] + }); + return test_authorized({ + first_factor: true, + second_factor: true, + userid: 'user', + allowed_domains: ['secret.example.com'] + }); + }); + it('should not be authenticated when session is partially initialized', function() { return test_unauthorized({ first_factor: true }); }); diff --git a/test/unitary/test_config_adapter.js b/test/unitary/test_config_adapter.js index 70335df5b..5ffcc84a3 100644 --- a/test/unitary/test_config_adapter.js +++ b/test/unitary/test_config_adapter.js @@ -27,11 +27,11 @@ describe('test config adapter', function() { var config = config_adapter(yaml_config); - assert.equal(config.ldap_url, 'http://ldap'); - assert.equal(config.ldap_user_search_base, 'ou=groups,dc=example,dc=com'); - assert.equal(config.ldap_user_search_filter, 'uid'); - assert.equal(config.ldap_user, 'admin'); - assert.equal(config.ldap_password, 'pass'); + assert.equal(config.ldap.url, 'http://ldap'); + assert.equal(config.ldap.user_search_base, 'ou=groups,dc=example,dc=com'); + assert.equal(config.ldap.user_search_filter, 'uid'); + assert.equal(config.ldap.user, 'admin'); + assert.equal(config.ldap.password, 'pass'); }); it('should get the session attributes', function() { @@ -64,4 +64,13 @@ describe('test config adapter', function() { assert.equal(config.notifier, 'notifier'); }); + + it('should get the access_control config', function() { + yaml_config = {}; + yaml_config.access_control = 'access_control'; + + var config = config_adapter(yaml_config); + + assert.equal(config.access_control, 'access_control'); + }); }); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index ebf5268a0..35c2c9807 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -9,6 +9,7 @@ var sinon = require('sinon'); var tmp = require('tmp'); var nedb = require('nedb'); var session = require('express-session'); +var winston = require('winston'); var PORT = 8050; var BASE_URL = 'http://localhost:' + PORT; @@ -21,8 +22,14 @@ describe('test data persistence', function() { var tmpDir; var ldap_client = { bind: sinon.stub(), - search: sinon.stub() + search: sinon.stub(), + on: sinon.spy() }; + var ldap = { + createClient: sinon.spy(function() { + return ldap_client; + }) + } var config; before(function() { @@ -55,7 +62,7 @@ describe('test data persistence', function() { totp_secret: 'totp_secret', ldap: { url: 'ldap://127.0.0.1:389', - user_search_base: 'ou=users,dc=example,dc=com', + base_dn: 'ou=users,dc=example,dc=com', }, session: { secret: 'session_secret', @@ -94,11 +101,13 @@ describe('test data persistence', function() { deps.nedb = nedb; deps.nodemailer = nodemailer; deps.session = session; + deps.winston = winston; + deps.ldapjs = ldap; var j1 = request.jar(); var j2 = request.jar(); - return start_server(config, ldap_client, deps) + return start_server(config, deps) .then(function(s) { server = s; return requests.login(j1); @@ -116,7 +125,7 @@ describe('test data persistence', function() { return stop_server(server); }) .then(function() { - return start_server(config, ldap_client, deps) + return start_server(config, deps) }) .then(function(s) { server = s; @@ -139,9 +148,9 @@ describe('test data persistence', function() { }); }); - function start_server(config, ldap_client, deps) { + function start_server(config, deps) { return new Promise(function(resolve, reject) { - var s = server.run(config, ldap_client, deps); + var s = server.run(config, deps); resolve(s); }); } diff --git a/test/unitary/test_ldap.js b/test/unitary/test_ldap.js index 9d301d239..c7fff8f61 100644 --- a/test/unitary/test_ldap.js +++ b/test/unitary/test_ldap.js @@ -1,185 +1,232 @@ -var ldap = require('../../src/lib/ldap'); +var Ldap = require('../../src/lib/ldap'); var sinon = require('sinon'); var Promise = require('bluebird'); var assert = require('assert'); +var ldapjs = require('ldapjs'); +var winston = require('winston'); describe('test ldap validation', function() { var ldap_client; + var ldap, ldapjs; + var ldap_config; beforeEach(function() { ldap_client = { bind: sinon.stub(), search: sinon.stub(), modify: sinon.stub(), - Change: sinon.spy() + on: sinon.stub() + }; + + ldapjs = { + Change: sinon.spy(), + createClient: sinon.spy(function() { + return ldap_client; +  }) } + ldap_config = { + url: 'http://localhost:324', + user: 'admin', + password: 'password', + base_dn: 'dc=example,dc=com', + additional_user_dn: 'ou=users' + }; + + var deps = {}; + deps.ldapjs = ldapjs; + deps.winston = winston; + + ldap = new Ldap(deps, ldap_config); + return ldap.connect(); }); describe('test binding', test_binding); - describe('test get email', test_get_email); + describe('test get emails from username', test_get_emails); + describe('test get groups from username', test_get_groups); describe('test update password', test_update_password); function test_binding() { - function test_validate() { - var username = 'user'; - var password = 'password'; - var users_dn = 'dc=example,dc=com'; - return ldap.validate(ldap_client, username, password, users_dn); + function test_bind() { + var username = "username"; + var password = "password"; + return ldap.bind(username, password); } it('should bind the user if good credentials provided', function() { ldap_client.bind.yields(); - return test_validate(); + return test_bind(); }); - it('should bind the user with correct DN', function(done) { + it('should bind the user with correct DN', function() { + ldap_config.user_name_attribute = 'uid'; var username = 'user'; var password = 'password'; - var user_search_base = 'dc=example,dc=com'; - var user_search_filter = 'uid'; - ldap_client.bind = sinon.spy(function(dn) { - if(dn == 'uid=user,dc=example,dc=com') done(); - }); - ldap.validate(ldap_client, username, password, user_search_base, - user_search_filter); + ldap_client.bind.withArgs('uid=user,ou=users,dc=example,dc=com').yields(); + return ldap.bind(username, password); }); - it('should default to cn user search filter if no filter provided', function(done) { + it('should default to cn user search filter if no filter provided', function() { var username = 'user'; var password = 'password'; - var user_search_base = 'dc=example,dc=com'; - ldap_client.bind = sinon.spy(function(dn) { - if(dn == 'cn=user,dc=example,dc=com') done(); - }); - ldap.validate(ldap_client, username, password, user_search_base, - undefined); - }); - - // cover an issue with promisify context - it('should promisify correctly', function() { - function LdapClient() { - this.test = 'abc'; - } - LdapClient.prototype.bind = function(username, password, fn) { - assert.equal('abc', this.test); - fn(); - } - ldap_client = new LdapClient(); - return test_validate(); + ldap_client.bind.withArgs('cn=user,ou=users,dc=example,dc=com').yields(); + return ldap.bind(username, password); }); it('should not bind the user if wrong credentials provided', function() { ldap_client.bind.yields('wrong credentials'); - var promise = test_validate(); + var promise = test_bind(); return promise.catch(function() { return Promise.resolve(); }); }); } - function test_get_email() { - it('should retrieve the email of an existing user', function() { - var expected_doc = {}; + function test_get_emails() { + var res_emitter; + var expected_doc; + + beforeEach(function() { + expected_doc = {}; expected_doc.object = {}; expected_doc.object.mail = 'user@example.com'; - var res_emitter = {}; + + res_emitter = {}; res_emitter.on = sinon.spy(function(event, fn) { if(event != 'error') fn(expected_doc) }); + }); + it('should retrieve the email of an existing user', function() { ldap_client.search.yields(undefined, res_emitter); - return ldap.get_email(ldap_client, 'user', 'dc=example,dc=com') - .then(function(doc) { - assert.deepEqual(doc, expected_doc.object); + return ldap.get_emails('user') + .then(function(emails) { + assert.deepEqual(emails, [expected_doc.object.mail]); return Promise.resolve(); }) }); - it('should use the user filter', function(done) { - ldap_client.search = sinon.spy(function(dn) { - if(dn == 'uid=username,ou=users,dc=example,dc=com') done(); + it('should retrieve email for user with uid name attribute', function() { + ldap_config.user_name_attribute = 'uid'; + ldap_client.search.withArgs('uid=username,ou=users,dc=example,dc=com').yields(undefined, res_emitter); + return ldap.get_emails('username') + .then(function(emails) { + assert.deepEqual(emails, ['user@example.com']); + return Promise.resolve(); }); - ldap.get_email(ldap_client, 'username', 'ou=users,dc=example,dc=com', - 'uid') }); - it('should fail on error with search method', function(done) { + it('should fail on error with search method', function() { var expected_doc = {}; expected_doc.mail = []; expected_doc.mail.push('user@example.com'); ldap_client.search.yields('error'); - ldap.get_email(ldap_client, 'user', 'dc=example,dc=com') + return ldap.get_emails('user') .catch(function() { + return Promise.resolve(); + }) + }); + } + + function test_get_groups() { + var res_emitter; + var expected_doc1, expected_doc2; + + beforeEach(function() { + expected_doc1 = {}; + expected_doc1.object = {}; + expected_doc1.object.cn = 'group1'; + + expected_doc2 = {}; + expected_doc2.object = {}; + expected_doc2.object.cn = 'group2'; + + res_emitter = {}; + res_emitter.on = sinon.spy(function(event, fn) { + if(event != 'error') fn(expected_doc1); + if(event != 'error') fn(expected_doc2); + }); + }); + + it('should retrieve the groups of an existing user', function() { + ldap_client.search.yields(undefined, res_emitter); + return ldap.get_groups('user') + .then(function(groups) { + assert.deepEqual(groups, ['group1', 'group2']); + return Promise.resolve(); + }); + }); + + it('should reduce the scope to additional_group_dn', function(done) { + ldap_config.additional_group_dn = 'ou=groups'; + ldap_client.search = sinon.spy(function(base_dn) { + assert.equal(base_dn, 'ou=groups,dc=example,dc=com'); done(); + }); + ldap.get_groups('user'); + }); + + it('should use default group_name_attr if not provided', function(done) { + ldap_client.search = sinon.spy(function(base_dn, query) { + assert.equal(base_dn, 'dc=example,dc=com'); + assert.equal(query.filter, 'member=cn=user,ou=users,dc=example,dc=com'); + assert.deepEqual(query.attributes, ['cn']); + done(); + }); + ldap.get_groups('user'); + }); + + it('should fail on error with search method', function() { + ldap_client.search.yields('error'); + return ldap.get_groups('user') + .catch(function() { + return Promise.resolve(); }) }); } function test_update_password() { - it('should update the password successfully', function(done) { + it('should update the password successfully', function() { var change = {}; change.operation = 'replace'; change.modification = {}; change.modification.userPassword = 'new-password'; - var config = {}; - config.ldap_user_search_base = 'dc=example,dc=com'; - config.ldap_user = 'admin'; - - var userdn = 'cn=user,dc=example,dc=com'; - - var ldapjs = {}; - ldapjs.Change = sinon.spy(); + var userdn = 'cn=user,ou=users,dc=example,dc=com'; ldap_client.bind.yields(undefined); ldap_client.modify.yields(undefined); - ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config) + return ldap.update_password('user', 'new-password') .then(function() { assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); assert.deepEqual(ldapjs.Change.getCall(0).args[0].operation, change.operation); var userPassword = ldapjs.Change.getCall(0).args[0].modification.userPassword; assert(/{SSHA}/.test(userPassword)); - done(); + return Promise.resolve(); }) }); - it('should fail when ldap throws an error', function(done) { + it('should fail when ldap throws an error', function() { ldap_client.bind.yields(undefined); ldap_client.modify.yields('Error'); - var config = {}; - config.ldap_users_dn = 'dc=example,dc=com'; - config.ldap_user = 'admin'; - - var ldapjs = {}; - ldapjs.Change = sinon.spy(); - - ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config) + return ldap.update_password('user', 'new-password') .catch(function() { - done(); + return Promise.resolve(); }) }); - it('should use the user filter', function(done) { - var ldapjs = {}; - ldapjs.Change = sinon.spy(); - - var config = {}; - config.ldap_user_search_base = 'ou=users,dc=example,dc=com'; - config.ldap_user_search_filter = 'uid'; - config.ldap_user = 'admin'; + it('should update password of user using particular user name attribute', function() { + ldap_config.user_name_attribute = 'uid'; ldap_client.bind.yields(undefined); - ldap_client.modify = sinon.spy(function(dn) { - if(dn == 'uid=username,ou=users,dc=example,dc=com') done(); - }); - ldap.update_password(ldap_client, ldapjs, 'username', 'newpass', config) + ldap_client.modify.withArgs('uid=username,ou=users,dc=example,dc=com').yields(); + return ldap.update_password('username', 'newpass'); }); } }); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index dea277b4d..33bf25c9b 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -1,5 +1,6 @@ var server = require('../../src/lib/server'); +var Ldap = require('../../src/lib/ldap'); var Promise = require('bluebird'); var request = Promise.promisifyAll(require('request')); @@ -8,6 +9,9 @@ var speakeasy = require('speakeasy'); var sinon = require('sinon'); var MockDate = require('mockdate'); var session = require('express-session'); +var winston = require('winston'); +var speakeasy = require('speakeasy'); +var ldapjs = require('ldapjs'); var PORT = 8090; var BASE_URL = 'http://localhost:' + PORT; @@ -19,14 +23,6 @@ describe('test the server', function() { var u2f, nedb; var transporter; var collection; - var ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - }; - var ldap = { - Change: sinon.spy() - } beforeEach(function(done) { var config = { @@ -34,8 +30,8 @@ describe('test the server', function() { totp_secret: 'totp_secret', ldap: { url: 'ldap://127.0.0.1:389', - user_search_base: 'ou=users,dc=example,dc=com', - user_search_filter: 'cn', + base_dn: 'ou=users,dc=example,dc=com', + user_name_attribute: 'cn', user: 'cn=admin,dc=example,dc=com', password: 'password', }, @@ -52,6 +48,19 @@ describe('test the server', function() { } }; + var ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.spy() + }; + var ldap = { + Change: sinon.spy(), + createClient: sinon.spy(function() { + return ldap_client; + }) + }; + u2f = {}; u2f.startRegistration = sinon.stub(); u2f.finishRegistration = sinon.stub(); @@ -68,15 +77,15 @@ describe('test the server', function() { return transporter;   }); - var search_doc = { + ldap_document = { object: { - mail: 'test_ok@example.com' + mail: 'test_ok@example.com', } }; var search_res = {}; search_res.on = sinon.spy(function(event, fn) { - if(event != 'error') fn(search_doc); + if(event != 'error') fn(ldap_document); }); ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com', @@ -94,10 +103,12 @@ describe('test the server', function() { deps.u2f = u2f; deps.nedb = nedb; deps.nodemailer = nodemailer; - deps.ldap = ldap; + deps.ldapjs = ldap; deps.session = session; + deps.winston = winston; + deps.speakeasy = speakeasy; - _server = server.run(config, ldap_client, deps, function() { + _server = server.run(config, deps, function() { done(); }); }); @@ -352,7 +363,6 @@ describe('test the server', function() { return requests.failing_first_factor(j); }) .then(function(res) { - console.log('coucou'); assert.equal(res.statusCode, 401, 'first factor failed'); return requests.failing_first_factor(j); }) diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js index 6f90f396b..b5c26b5e3 100644 --- a/test/unitary/test_server_config.js +++ b/test/unitary/test_server_config.js @@ -26,7 +26,12 @@ describe('test server configuration', function() { deps = {}; deps.nedb = require('nedb'); + deps.winston = sinon.spy(); deps.nodemailer = nodemailer; + deps.ldapjs = {}; + deps.ldapjs.createClient = sinon.spy(function() { + return { on: sinon.spy() }; + }); deps.session = sinon.spy(function() { return function(req, res, next) { next(); }; }); @@ -36,7 +41,9 @@ describe('test server configuration', function() { it('should set cookie scope to domain set in the config', function() { config.session = {}; config.session.domain = 'example.com'; - server.run(config, undefined, deps); + config.ldap = {}; + config.ldap.url = 'http://ldap'; + server.run(config, deps); assert(deps.session.calledOnce); assert.equal(deps.session.getCall(0).args[0].cookie.domain, 'example.com');