Skip to content

Commit f53195d

Browse files
committed
feat(auth): add support for SCRAM-SHA-256
Adds support for SCRAM-SHA-256 and auth mechanism negotiation Also implements prose tests for auth, and fixes a few bugs Fixes NODE-1311 Fixes NODE-1303 Fixes NODE-1413
1 parent d6c3417 commit f53195d

File tree

7 files changed

+359
-8
lines changed

7 files changed

+359
-8
lines changed

lib/authenticate.js

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ var authenticate = function(client, username, password, options, callback) {
5151
if (err) return handleCallback(callback, err, false);
5252
_callback(null, true);
5353
});
54+
} else if (authMechanism === 'SCRAM-SHA-256') {
55+
client.topology.auth('scram-sha-256', authdb, username, password, function(err) {
56+
if (err) return handleCallback(callback, err, false);
57+
_callback(null, true);
58+
});
5459
} else if (authMechanism === 'GSSAPI') {
5560
if (process.platform === 'win32') {
5661
client.topology.auth('sspi', authdb, username, password, options, function(err) {
@@ -95,6 +100,7 @@ module.exports = function(self, username, password, options, callback) {
95100
options.authMechanism !== 'MONGODB-CR' &&
96101
options.authMechanism !== 'MONGODB-X509' &&
97102
options.authMechanism !== 'SCRAM-SHA-1' &&
103+
options.authMechanism !== 'SCRAM-SHA-256' &&
98104
options.authMechanism !== 'PLAIN'
99105
) {
100106
return handleCallback(

lib/db.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1189,22 +1189,28 @@ var _executeAuthCreateUserCommand = function(self, username, password, options,
11891189
roles = ['dbOwner'];
11901190
}
11911191

1192+
const digestPassword = self.s.topology.lastIsMaster().maxWireVersion >= 7;
1193+
11921194
// Build the command to execute
11931195
var command = {
11941196
createUser: username,
11951197
customData: customData,
11961198
roles: roles,
1197-
digestPassword: false
1199+
digestPassword
11981200
};
11991201

12001202
// Apply write concern to command
12011203
command = applyWriteConcern(command, { db: self }, options);
12021204

1203-
// Use node md5 generator
1204-
var md5 = crypto.createHash('md5');
1205-
// Generate keys used for authentication
1206-
md5.update(username + ':mongo:' + password);
1207-
var userPassword = md5.digest('hex');
1205+
let userPassword = password;
1206+
1207+
if (!digestPassword) {
1208+
// Use node md5 generator
1209+
let md5 = crypto.createHash('md5');
1210+
// Generate keys used for authentication
1211+
md5.update(username + ':mongo:' + password);
1212+
userPassword = md5.digest('hex');
1213+
}
12081214

12091215
// No password
12101216
if (typeof password === 'string') {

lib/mongo_client.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ function createServer(self, options, callback) {
778778
var collectedEvents = collectEvents(self, servers[0]);
779779

780780
// Connect to topology
781-
servers[0].connect(function(err, topology) {
781+
servers[0].connect(options, function(err, topology) {
782782
if (err) return callback(err);
783783
// Clear out all the collected event listeners
784784
clearAllEvents(servers[0]);

lib/url_parser.js

+1
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ function parseConnectionString(url, options) {
484484
value !== 'MONGODB-CR' &&
485485
value !== 'DEFAULT' &&
486486
value !== 'SCRAM-SHA-1' &&
487+
value !== 'SCRAM-SHA-256' &&
487488
value !== 'PLAIN'
488489
)
489490
throw new Error(

test/functional/saslprep_tests.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
const setupDatabase = require('./shared').setupDatabase;
4+
const withClient = require('./shared').withClient;
5+
6+
describe('SASLPrep', function() {
7+
// Step 4
8+
// To test SASLprep behavior, create two users:
9+
// username: "IX", password "IX"
10+
// username: "u2168" (ROMAN NUMERAL NINE), password "u2163" (ROMAN NUMERAL FOUR)
11+
// To create the users, use the exact bytes for username and password without SASLprep or other normalization and specify SCRAM-SHA-256 credentials:
12+
// db.runCommand({createUser: 'IX', pwd: 'IX', roles: ['root'], mechanisms: ['SCRAM-SHA-256']}) db.runCommand({createUser: 'u2168', pwd: 'u2163', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
13+
// For each user, verify that the driver can authenticate with the password in both SASLprep normalized and non-normalized forms:
14+
// User "IX": use password forms "IX" and "Iu00ADX"
15+
// User "u2168": use password forms "IV" and "Iu00ADV"
16+
// As a URI, those have to be UTF-8 encoded and URL-escaped, e.g.:
17+
// mongodb://IX:IX@mongodb.example.com/admin
18+
// mongodb://IX:I%C2%[email protected]/admin
19+
// mongodb://%E2%85%A8:[email protected]/admin
20+
// mongodb://%E2%85%A8:I%C2%[email protected]/admin
21+
22+
const users = [
23+
{
24+
username: 'IX',
25+
password: 'IX',
26+
mechanisms: ['SCRAM-SHA-256']
27+
},
28+
{
29+
username: '\u2168',
30+
password: '\u2163',
31+
mechanisms: ['SCRAM-SHA-256']
32+
}
33+
];
34+
35+
before(function() {
36+
return setupDatabase(this.configuration);
37+
});
38+
39+
before(function() {
40+
return withClient(this.configuration.newClient(), client => {
41+
const db = client.db('admin');
42+
43+
const createUserCommands = users.map(user => ({
44+
createUser: user.username,
45+
pwd: user.password,
46+
roles: ['root'],
47+
mechanisms: user.mechanisms
48+
}));
49+
50+
return Promise.all(createUserCommands.map(cmd => db.command(cmd)));
51+
});
52+
});
53+
54+
after(function() {
55+
return withClient(this.configuration.newClient(), client => {
56+
const db = client.db('admin');
57+
58+
return Promise.all(users.map(user => db.removeUser(user.username)));
59+
});
60+
});
61+
62+
[
63+
{ username: 'IX', password: 'IX' },
64+
{ username: 'IX', password: 'I\u00ADX' },
65+
{ username: 'IX', password: '\u2168' },
66+
{ username: '\u2168', password: 'IV' },
67+
{ username: '\u2168', password: 'I\u00ADV' },
68+
{ username: '\u2168', password: '\u2163' }
69+
].forEach(user => {
70+
const username = user.username;
71+
const password = user.password;
72+
73+
it(`should be able to login with username "${username}" and password "${password}"`, {
74+
metadata: {
75+
requires: {
76+
mongodb: '>=3.7.3',
77+
node: '>=6'
78+
}
79+
},
80+
test: function() {
81+
const options = {
82+
user: username,
83+
password: password,
84+
authSource: 'admin',
85+
authMechanism: 'SCRAM-SHA-256'
86+
};
87+
88+
return withClient(this.configuration.newClient(options), client => {
89+
return client.db('admin').stats();
90+
});
91+
}
92+
});
93+
});
94+
});
+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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

Comments
 (0)