Skip to content

Commit da58b3a

Browse files
Corrected handling of HTTP Basic edge cases
Previous implementation: empty user-pass -> error basic realm no " " separator -> error 400 no : separator -> error basic realm empty password -> error basic realm empty username -> error basic realm New implementation: empty user-pass -> error 400 no " " separator -> error 400 no : separator -> error 400 empty password -> success empty username -> success Also fixed passwords containing ':' being truncated. This fixes: - jaredhanson/passport-http#20 - jaredhanson/passport-http#41 - jaredhanson/passport-http#42 - jaredhanson/passport-http#63 - jaredhanson/passport-http#78 The new implemementation complies with https://tools.ietf.org/html/rfc2617#section-2.
1 parent 07fd716 commit da58b3a

File tree

4 files changed

+187
-14
lines changed

4 files changed

+187
-14
lines changed

lib/strategies/basic.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
const passport = require('passport-strategy');
55
const util = require('util');
6+
const { splitFirst } = require('../string.util');
67

78
/**
89
* `BasicStrategy` constructor.
@@ -67,19 +68,14 @@ BasicStrategy.prototype.authenticate = function (req) {
6768

6869
const scheme = parts[0];
6970
const userPass = Buffer.from(parts[1], 'base64').toString();
70-
const credentials = userPass.split(':');
71+
const [userid, password] = splitFirst(userPass, ':');
7172

7273
if (!/Basic/i.test(scheme)) {
7374
return this.fail(this._challenge());
7475
}
75-
if (credentials.length < 2) {
76-
return this.fail(400);
77-
}
7876

79-
const userid = credentials[0];
80-
const password = credentials[1];
81-
if (!userid || !password) {
82-
return this.fail(this._challenge());
77+
if (userid === undefined || password === undefined) {
78+
return this.fail(400);
8379
}
8480

8581
const self = this;

lib/string.util.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
3+
/**
4+
* Splits a string on the first occurrence of the provided separator.
5+
* Returns an array with one or two elements.
6+
* @param {String} string
7+
* @param {String} separator
8+
*/
9+
splitFirst: (string, separator) => {
10+
const separatorIndex = string.indexOf(separator);
11+
12+
if (separatorIndex < 0) {
13+
return [string];
14+
}
15+
16+
return [
17+
string.substring(0, separatorIndex),
18+
string.substring(separatorIndex + 1),
19+
];
20+
},
21+
};

test/strategies/basic-test.js

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ vows.describe('BasicStrategy').addBatch({
179179
},
180180
},
181181

182-
'strategy handling a request with credentials lacking a password': {
182+
'strategy handling a request with credentials lacking the " " separator': {
183183
topic: function () {
184184
return new BasicStrategy(((userid, password, done) => {
185185
done(null, {username: userid, password: password});
@@ -197,7 +197,7 @@ vows.describe('BasicStrategy').addBatch({
197197
};
198198

199199
req.headers = {};
200-
req.headers.authorization = 'Basic Ym9iOg==';
200+
req.headers.authorization = 'Basic';
201201
process.nextTick(() => {
202202
strategy.authenticate(req);
203203
});
@@ -206,12 +206,12 @@ vows.describe('BasicStrategy').addBatch({
206206
'should fail authentication with challenge': function (err, challenge) {
207207
// fail action was called, resulting in test callback
208208
assert.isNull(err);
209-
assert.strictEqual(challenge, 'Basic realm="Users"');
209+
assert.strictEqual(challenge, 400);
210210
},
211211
},
212212
},
213213

214-
'strategy handling a request with credentials lacking a username': {
214+
'strategy handling a request with credentials containing an empty user-pass': {
215215
topic: function () {
216216
return new BasicStrategy(((userid, password, done) => {
217217
done(null, {username: userid, password: password});
@@ -229,7 +229,7 @@ vows.describe('BasicStrategy').addBatch({
229229
};
230230

231231
req.headers = {};
232-
req.headers.authorization = 'Basic OnNlY3JldA==';
232+
req.headers.authorization = 'Basic ';
233233
process.nextTick(() => {
234234
strategy.authenticate(req);
235235
});
@@ -238,7 +238,139 @@ vows.describe('BasicStrategy').addBatch({
238238
'should fail authentication with challenge': function (err, challenge) {
239239
// fail action was called, resulting in test callback
240240
assert.isNull(err);
241-
assert.strictEqual(challenge, 'Basic realm="Users"');
241+
assert.strictEqual(challenge, 400);
242+
},
243+
},
244+
},
245+
246+
'strategy handling a request with credentials lacking the ":" separator': {
247+
topic: function () {
248+
return new BasicStrategy(((userid, password, done) => {
249+
done(null, {username: userid, password: password});
250+
}));
251+
},
252+
253+
'after augmenting with actions': {
254+
topic: function (strategy) {
255+
const req = {};
256+
strategy.success = (user) => {
257+
this.callback(new Error('should not be called'));
258+
};
259+
strategy.fail = (challenge) => {
260+
this.callback(null, challenge);
261+
};
262+
263+
req.headers = {};
264+
req.headers.authorization = 'Basic Ym9i'; // bob
265+
process.nextTick(() => {
266+
strategy.authenticate(req);
267+
});
268+
},
269+
270+
'should fail authentication with challenge': function (err, challenge) {
271+
// fail action was called, resulting in test callback
272+
assert.isNull(err);
273+
assert.strictEqual(challenge, 400);
274+
},
275+
},
276+
},
277+
278+
'strategy handling a request with credentials containing an empty username': {
279+
topic: function () {
280+
return new BasicStrategy(((userid, password, done) => {
281+
done(null, {username: userid, password: password});
282+
}));
283+
},
284+
285+
'after augmenting with actions': {
286+
topic: function (strategy) {
287+
const req = {};
288+
strategy.success = (user) => {
289+
this.callback(null, user);
290+
};
291+
strategy.fail = (challenge) => {
292+
this.callback(new Error('should not be called'));
293+
};
294+
295+
req.headers = {};
296+
req.headers.authorization = 'Basic OnBhc3N3b3Jk'; // :password
297+
process.nextTick(() => {
298+
strategy.authenticate(req);
299+
});
300+
},
301+
302+
'should not generate an error': (err, user) => {
303+
assert.isNull(err);
304+
},
305+
'should authenticate': (err, user) => {
306+
assert.strictEqual(user.username, '');
307+
assert.strictEqual(user.password, 'password');
308+
},
309+
},
310+
},
311+
312+
'strategy handling a request with credentials containing an empty password': {
313+
topic: function () {
314+
return new BasicStrategy(((userid, password, done) => {
315+
done(null, {username: userid, password: password});
316+
}));
317+
},
318+
319+
'after augmenting with actions': {
320+
topic: function (strategy) {
321+
const req = {};
322+
strategy.success = (user) => {
323+
this.callback(null, user);
324+
};
325+
strategy.fail = (challenge) => {
326+
this.callback(new Error('should not be called'));
327+
};
328+
329+
req.headers = {};
330+
req.headers.authorization = 'Basic Ym9iOg=='; // bob:
331+
process.nextTick(() => {
332+
strategy.authenticate(req);
333+
});
334+
},
335+
336+
'should not generate an error': (err, user) => {
337+
assert.isNull(err);
338+
},
339+
'should authenticate': (err, user) => {
340+
assert.strictEqual(user.username, 'bob');
341+
assert.strictEqual(user.password, '');
342+
},
343+
},
344+
},
345+
346+
'strategy handling a request containing a colon in the password': {
347+
topic: function () {
348+
return new BasicStrategy((userid, password, done) => {
349+
done(null, {username: userid, password: password});
350+
});
351+
},
352+
'after augmenting with actions': {
353+
topic: function (strategy) {
354+
const req = {};
355+
strategy.success = user => {
356+
this.callback(null, user);
357+
};
358+
strategy.fail = () => {
359+
this.callback(new Error('should not be called'));
360+
};
361+
362+
req.headers = {};
363+
req.headers.authorization = 'Basic Ym9iOnNlY3JldDpwdw=='; // bob:secret:pw
364+
process.nextTick(() => {
365+
strategy.authenticate(req);
366+
});
367+
},
368+
'should not generate an error': (err, user) => {
369+
assert.isNull(err);
370+
},
371+
'should authenticate': (err, user) => {
372+
assert.strictEqual(user.username, 'bob');
373+
assert.strictEqual(user.password, 'secret:pw');
242374
},
243375
},
244376
},

test/string.util-test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const vows = require('vows');
2+
const assert = require('assert');
3+
const stringUtils = require('../lib/string.util');
4+
const {splitFirst} = stringUtils;
5+
6+
vows.describe('string utils').addBatch({
7+
8+
'module': {
9+
'should export splitFirst': () => {
10+
assert.isFunction(stringUtils.splitFirst);
11+
},
12+
},
13+
14+
'splitFirst': {
15+
'should split only first': () => {
16+
assert.deepStrictEqual(splitFirst('bob:secret:pw', ':'), ['bob', 'secret:pw']);
17+
assert.deepStrictEqual(splitFirst('a:bb:a:d', ':'), ['a', 'bb:a:d']);
18+
},
19+
'should handle non-existing seperator': () => {
20+
assert.deepStrictEqual(splitFirst('abc', ':'), ['abc']);
21+
},
22+
},
23+
24+
}).export(module);

0 commit comments

Comments
 (0)