|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const expect = require('chai').expect; |
| 4 | +const sinon = require('sinon'); |
| 5 | +const ScramSHA256 = require('mongodb-core').ScramSHA256; |
| 6 | +const MongoError = require('mongodb-core').MongoError; |
| 7 | +const setupDatabase = require('./shared').setupDatabase; |
| 8 | +const withClient = require('./shared').withClient; |
| 9 | +const MongoClient = require('../../lib/mongo_client'); |
| 10 | + |
| 11 | +describe('SCRAM-SHA-256 auth', function() { |
| 12 | + const test = {}; |
| 13 | + const userMap = { |
| 14 | + sha1: { |
| 15 | + description: 'user with sha1 credentials', |
| 16 | + username: 'sha1', |
| 17 | + password: 'sha1', |
| 18 | + mechanisms: ['SCRAM-SHA-1'] |
| 19 | + }, |
| 20 | + sha256: { |
| 21 | + description: 'user with sha256 credentials', |
| 22 | + username: 'sha256', |
| 23 | + password: 'sha256', |
| 24 | + mechanisms: ['SCRAM-SHA-256'] |
| 25 | + }, |
| 26 | + both: { |
| 27 | + description: 'user with both credentials', |
| 28 | + username: 'both', |
| 29 | + password: 'both', |
| 30 | + mechanisms: ['SCRAM-SHA-1', 'SCRAM-SHA-256'] |
| 31 | + } |
| 32 | + }; |
| 33 | + |
| 34 | + function makeConnectionString(config, username, password) { |
| 35 | + return `mongodb://${username}:${password}@${config.host}:${config.port}/${config.db}?`; |
| 36 | + } |
| 37 | + |
| 38 | + const users = Object.keys(userMap).map(name => userMap[name]); |
| 39 | + |
| 40 | + afterEach(() => test.sandbox.restore()); |
| 41 | + |
| 42 | + before(function() { |
| 43 | + test.sandbox = sinon.sandbox.create(); |
| 44 | + return setupDatabase(this.configuration); |
| 45 | + }); |
| 46 | + |
| 47 | + before(function() { |
| 48 | + return withClient(this.configuration.newClient(), client => { |
| 49 | + test.oldDbName = this.configuration.db; |
| 50 | + this.configuration.db = 'admin'; |
| 51 | + const db = client.db(this.configuration.db); |
| 52 | + |
| 53 | + const createUserCommands = users.map(user => ({ |
| 54 | + createUser: user.username, |
| 55 | + pwd: user.password, |
| 56 | + roles: ['root'], |
| 57 | + mechanisms: user.mechanisms |
| 58 | + })); |
| 59 | + |
| 60 | + return Promise.all(createUserCommands.map(cmd => db.command(cmd))); |
| 61 | + }); |
| 62 | + }); |
| 63 | + |
| 64 | + after(function() { |
| 65 | + return withClient(this.configuration.newClient(), client => { |
| 66 | + const db = client.db(this.configuration.db); |
| 67 | + this.configuration.db = test.oldDbName; |
| 68 | + |
| 69 | + return Promise.all(users.map(user => db.removeUser(user.username))); |
| 70 | + }); |
| 71 | + }); |
| 72 | + |
| 73 | + // Step 2 |
| 74 | + // For each test user, verify that you can connect and run a command requiring authentication for the following cases: |
| 75 | + // Explicitly specifying each mechanism the user supports. |
| 76 | + // Specifying no mechanism and relying on mechanism negotiation. |
| 77 | + // For the example users above, the dbstats command could be used as a test command. |
| 78 | + users.forEach(user => { |
| 79 | + user.mechanisms.forEach(mechanism => { |
| 80 | + it(`should auth ${user.description} when explicitly specifying ${mechanism}`, { |
| 81 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 82 | + test: function() { |
| 83 | + const options = { |
| 84 | + user: user.username, |
| 85 | + password: user.password, |
| 86 | + authMechanism: mechanism, |
| 87 | + authSource: this.configuration.db |
| 88 | + }; |
| 89 | + |
| 90 | + return withClient(this.configuration.newClient(options), client => { |
| 91 | + return client.db(this.configuration.db).stats(); |
| 92 | + }); |
| 93 | + } |
| 94 | + }); |
| 95 | + |
| 96 | + it(`should auth ${user.description} when explicitly specifying ${mechanism} in url`, { |
| 97 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 98 | + test: function() { |
| 99 | + const username = encodeURIComponent(user.username); |
| 100 | + const password = encodeURIComponent(user.password); |
| 101 | + |
| 102 | + const url = `${makeConnectionString( |
| 103 | + this.configuration, |
| 104 | + username, |
| 105 | + password |
| 106 | + )}authMechanism=${mechanism}`; |
| 107 | + |
| 108 | + const client = new MongoClient(url); |
| 109 | + |
| 110 | + return withClient(client, client => { |
| 111 | + return client.db(this.configuration.db).stats(); |
| 112 | + }); |
| 113 | + } |
| 114 | + }); |
| 115 | + }); |
| 116 | + |
| 117 | + it(`should auth ${user.description} using mechanism negotiaton`, { |
| 118 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 119 | + test: function() { |
| 120 | + const options = { |
| 121 | + user: user.username, |
| 122 | + password: user.password, |
| 123 | + authSource: this.configuration.db |
| 124 | + }; |
| 125 | + |
| 126 | + return withClient(this.configuration.newClient(options), client => { |
| 127 | + return client.db(this.configuration.db).stats(); |
| 128 | + }); |
| 129 | + } |
| 130 | + }); |
| 131 | + |
| 132 | + it(`should auth ${user.description} using mechanism negotiaton and url`, { |
| 133 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 134 | + test: function() { |
| 135 | + const username = encodeURIComponent(user.username); |
| 136 | + const password = encodeURIComponent(user.password); |
| 137 | + const url = makeConnectionString(this.configuration, username, password); |
| 138 | + |
| 139 | + const client = new MongoClient(url); |
| 140 | + |
| 141 | + return withClient(client, client => { |
| 142 | + return client.db(this.configuration.db).stats(); |
| 143 | + }); |
| 144 | + } |
| 145 | + }); |
| 146 | + }); |
| 147 | + |
| 148 | + // For a test user supporting both SCRAM-SHA-1 and SCRAM-SHA-256, |
| 149 | + // drivers should verify that negotation selects SCRAM-SHA-256.. |
| 150 | + it('should select SCRAM-SHA-256 for a user that supports both auth mechanisms', { |
| 151 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 152 | + test: function() { |
| 153 | + const options = { |
| 154 | + user: userMap.both.username, |
| 155 | + password: userMap.both.password, |
| 156 | + authSource: this.configuration.db |
| 157 | + }; |
| 158 | + |
| 159 | + test.sandbox.spy(ScramSHA256.prototype, 'auth'); |
| 160 | + |
| 161 | + return withClient(this.configuration.newClient(options), () => { |
| 162 | + expect(ScramSHA256.prototype.auth.calledOnce).to.equal(true); |
| 163 | + }); |
| 164 | + } |
| 165 | + }); |
| 166 | + |
| 167 | + // Step 3 |
| 168 | + // For test users that support only one mechanism, verify that explictly specifying the other mechanism fails. |
| 169 | + it('should fail to connect if incorrect auth mechanism is explicitly specified', { |
| 170 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 171 | + test: function() { |
| 172 | + const options = { |
| 173 | + user: userMap.sha256.username, |
| 174 | + password: userMap.sha256.password, |
| 175 | + authSource: this.configuration.db, |
| 176 | + authMechanism: 'SCRAM-SHA-1' |
| 177 | + }; |
| 178 | + |
| 179 | + return withClient( |
| 180 | + this.configuration.newClient(options), |
| 181 | + () => Promise.reject('This request should have failed to authenticate'), |
| 182 | + err => expect(err).to.not.be.null |
| 183 | + ); |
| 184 | + } |
| 185 | + }); |
| 186 | + |
| 187 | + // For a non-existent username, verify that not specifying a mechanism when connecting fails with the same error |
| 188 | + // type that would occur with a correct username but incorrect password or mechanism. (Because negotiation with |
| 189 | + // a non-existent user name causes an isMaster error, we want to verify this is seen by users as similar to other |
| 190 | + // authentication errors, not as a network or database command error.) |
| 191 | + it('should fail for a nonexistent username with same error type as bad password', { |
| 192 | + metadata: { requires: { mongodb: '>=3.7.3' } }, |
| 193 | + test: function() { |
| 194 | + const noUsernameOptions = { |
| 195 | + user: 'roth', |
| 196 | + password: 'pencil', |
| 197 | + authSource: 'admin' |
| 198 | + }; |
| 199 | + |
| 200 | + const badPasswordOptions = { |
| 201 | + user: 'both', |
| 202 | + password: 'pencil', |
| 203 | + authSource: 'admin' |
| 204 | + }; |
| 205 | + |
| 206 | + const getErrorMsg = options => |
| 207 | + withClient( |
| 208 | + this.configuration.newClient(options), |
| 209 | + () => Promise.reject('This request should have failed to authenticate'), |
| 210 | + err => expect(err).to.be.an.instanceof(MongoError) |
| 211 | + ); |
| 212 | + |
| 213 | + return Promise.all([getErrorMsg(noUsernameOptions), getErrorMsg(badPasswordOptions)]); |
| 214 | + } |
| 215 | + }); |
| 216 | +}); |
0 commit comments