handle undefined credential more gracefully
[squeep-authentication-module] / test / lib / authenticator.js
1 /* eslint-env mocha */
2 'use strict';
3
4 const assert = require('assert');
5 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
6 const Authenticator = require('../../lib/authenticator');
7 const stubLogger = require('../stub-logger');
8 const stubDb = require('../stub-db');
9 const Errors = require('../../lib/errors');
10 const Enum = require('../../lib/enum');
11 const Config = require('../stub-config');
12
13 const noExpectedException = 'did not receive expected exception';
14
15 describe('Authenticator', function () {
16 let authenticator, credential, ctx, identifier, password, options;
17 function _authMechanismRequired(a, m) {
18 if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
19 this.skip();
20 }
21 }
22
23 beforeEach(function () {
24 options = Config('test');
25 authenticator = new Authenticator(stubLogger, stubDb, options);
26 identifier = 'username';
27 credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
28 ctx = {};
29 password = 'badPassword';
30 stubDb._reset();
31 });
32 afterEach(function () {
33 sinon.restore();
34 });
35
36 it('covers no auth mechanisms', function () {
37 options.authenticator.authnEnabled = [];
38 try {
39 authenticator = new Authenticator(stubLogger, stubDb, options);
40 assert.fail(noExpectedException);
41 } catch (e) {
42 assert.strictEqual(e.message, 'no authentication mechanisms available');
43 }
44 });
45
46 it('covers empty realm', function () {
47 options.authenticator.basicRealm = undefined;
48 authenticator = new Authenticator(stubLogger, stubDb, options);
49 });
50
51 describe('isValidBasic', function () {
52 it('succeeds', async function () {
53 _authMechanismRequired(authenticator, 'argon2');
54 authenticator.db.authenticationGet.resolves({
55 identifier,
56 credential,
57 });
58 const authString = `${identifier}:${password}`;
59 const result = await authenticator.isValidBasic(authString, ctx);
60 assert.strictEqual(result, true);
61 assert.strictEqual(ctx.authenticationId, identifier);
62 });
63 it('fails', async function () {
64 _authMechanismRequired(authenticator, 'argon2');
65 authenticator.db.authenticationGet.resolves({
66 identifier,
67 credential,
68 });
69 const authString = `${identifier}:wrongPassword}`;
70 const result = await authenticator.isValidBasic(authString, ctx);
71 assert.strictEqual(result, false);
72 assert.strictEqual(ctx.authenticationId, undefined);
73 });
74 it('covers no entry', async function() {
75 authenticator.db.authenticationGet.resolves();
76 const authString = `${identifier}:wrongPassword}`;
77 const result = await authenticator.isValidBasic(authString, ctx);
78 assert.strictEqual(result, false);
79 assert.strictEqual(ctx.authenticationId, undefined);
80 });
81 it('covers unknown password hash', async function () {
82 authenticator.db.authenticationGet.resolves({
83 identifier,
84 credential: '$other$kind_of_credential',
85 });
86 const authString = `${identifier}:wrongPassword}`;
87 const result = await authenticator.isValidBasic(authString, ctx);
88 assert.strictEqual(result, false);
89 assert.strictEqual(ctx.authenticationId, undefined);
90 });
91 }); // isValidBasic
92
93 describe('isValidIdentifierCredential', function () {
94 it('succeeds', async function () {
95 _authMechanismRequired(authenticator, 'argon2');
96 authenticator.db.authenticationGet.resolves({
97 identifier,
98 credential,
99 });
100 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
101 assert.strictEqual(result, true);
102 assert.strictEqual(ctx.authenticationId, identifier);
103 });
104 it('fails', async function () {
105 _authMechanismRequired(authenticator, 'argon2');
106 authenticator.db.authenticationGet.resolves({
107 identifier,
108 credential,
109 });
110 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
111 assert.strictEqual(result, false);
112 assert.strictEqual(ctx.authenticationId, undefined);
113 });
114 it('covers no entry', async function() {
115 authenticator.db.authenticationGet.resolves();
116 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
117 assert.strictEqual(result, false);
118 assert.strictEqual(ctx.authenticationId, undefined);
119 });
120 it('covers non-string credential', async function () {
121 credential = '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
122 const result = await authenticator.isValidIdentifierCredential(identifier, undefined, ctx);
123 assert.strictEqual(result, false);
124 assert.strictEqual(ctx.authenticationId, undefined);
125 });
126 it('covers unknown password hash', async function () {
127 authenticator.db.authenticationGet.resolves({
128 identifier,
129 credential: '$other$kind_of_credential',
130 });
131 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
132 assert.strictEqual(result, false);
133 assert.strictEqual(ctx.authenticationId, undefined);
134 });
135 it('covers PAM', async function () {
136 _authMechanismRequired(authenticator, 'pam');
137 sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
138 authenticator.db.authenticationGet.resolves({
139 identifier,
140 credential: '$PAM$',
141 });
142 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
143 assert.strictEqual(result, true);
144 assert.strictEqual(ctx.authenticationId, identifier);
145 });
146 it('covers debug', async function () {
147 authenticator.authnEnabled = ['DEBUG_ANY'];
148 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
149 assert.strictEqual(result, true);
150 assert.strictEqual(ctx.authenticationId, identifier);
151 });
152 }); // isValidIdentifierCredential
153
154 describe('_isValidPAMIdentifier', function () {
155 beforeEach(function () {
156 _authMechanismRequired(authenticator, 'pam');
157 sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
158 });
159 it('covers success', async function () {
160 authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
161 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
162 assert.strictEqual(result, true);
163 });
164 it('covers failure', async function () {
165 _authMechanismRequired(authenticator, 'pam');
166 authenticator.authn.pam.pamAuthenticatePromise.rejects(new authenticator.authn.pam.PamError());
167 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
168 assert.strictEqual(result, false);
169 });
170 it('covers error', async function () {
171 _authMechanismRequired(authenticator, 'pam');
172 const expected = new Error('blah');
173 authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
174 try {
175 await authenticator._isValidPAMIdentifier(identifier, credential);
176 assert.fail(noExpectedException);
177 } catch (e) {
178 assert.deepStrictEqual(e, expected);
179 }
180 });
181 it('covers forbidden', async function () {
182 identifier = 'root';
183 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
184 assert.strictEqual(result, false);
185 });
186 }); // _isValidPAMIdentifier
187
188 describe('_cookieParse', function () {
189 it('covers empty', function () {
190 const expected = {};
191 const result = Authenticator._cookieParse();
192 assert.deepStrictEqual(result, expected);
193 });
194 it('covers non variable', function () {
195 const cookie = 'foo';
196 const expected = {
197 foo: null,
198 };
199 const result = Authenticator._cookieParse(cookie);
200 assert.deepStrictEqual(result, expected);
201 });
202 it('parses cookie', function () {
203 const cookie = 'foo=bar; baz="quux"';
204 const expected = {
205 foo: 'bar',
206 baz: 'quux',
207 };
208 const result = Authenticator._cookieParse(cookie);
209 assert.deepStrictEqual(result, expected);
210 });
211 }); // _cookieParse
212
213 describe('isValidAuthorization', function () {
214 it('handles basic', async function () {
215 const expected = true;
216 const authorizationHeader = 'basic Zm9vOmJhcg==';
217 sinon.stub(authenticator, 'isValidBasic').resolves(expected);
218 const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
219 assert.strictEqual(result, expected);
220 });
221 it('handles other', async function () {
222 const expected = false;
223 const authorizationHeader = 'bearer Zm9vOmJhcg==';
224 const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
225 assert.strictEqual(result, expected);
226 });
227 }); // isValidAuthorization
228
229 describe('requestBasic', function () {
230 it('covers', function () {
231 try {
232 const res = {
233 setHeader: () => {},
234 };
235 authenticator.requestBasic(res);
236 assert.fail(noExpectedException);
237 } catch (e) {
238 assert(e instanceof Errors.ResponseError);
239 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
240 }
241 });
242 }); // requestBasic
243
244 describe('isValidCookieAuth', function () {
245 let cookie;
246 beforeEach(function () {
247 sinon.stub(authenticator.mysteryBox, 'unpack');
248 cookie = 'squeepSession=dummy';
249 });
250 it('covers identifier success', async function () {
251 authenticator.mysteryBox.unpack.resolves({
252 authenticatedIdentifier: 'identifier',
253 });
254 const result = await authenticator.isValidCookieAuth(ctx, cookie);
255 assert.strictEqual(result, true);
256 });
257 it('covers profile success', async function () {
258 authenticator.mysteryBox.unpack.resolves({
259 authenticatedProfile: 'profile',
260 });
261 const result = await authenticator.isValidCookieAuth(ctx, cookie);
262 assert.strictEqual(result, true);
263 });
264 it('covers missing cookie', async function () {
265 cookie = 'wrong=cookie';
266 const result = await authenticator.isValidCookieAuth(ctx, cookie);
267 assert.strictEqual(result, false);
268 });
269 it('covers bad cookie', async function () {
270 authenticator.mysteryBox.unpack.rejects();
271 const result = await authenticator.isValidCookieAuth(ctx, cookie);
272 assert.strictEqual(result, false);
273 });
274 it('covers broken session', async function () {
275 authenticator.mysteryBox.unpack.resolves({
276 randomData: 'foo',
277 });
278 const result = await authenticator.isValidCookieAuth(ctx, cookie);
279 assert.strictEqual(result, false);
280 });
281 }); // isValidCookieAuth
282
283 describe('sessionCheck', function () {
284 let cookie, req, res, loginPath, required, profilesAllowed;
285 beforeEach(function () {
286 cookie = 'squeepSession=sessionCookie';
287 ctx.clientProtocol = 'https';
288 req = {
289 getHeader: sinon.stub(),
290 };
291 res = {
292 end: sinon.stub(),
293 setHeader: sinon.stub(),
294 };
295 loginPath = '/admin/login';
296 required = true;
297 profilesAllowed = true;
298 });
299 it('covers valid cookie session', async function () {
300 req.getHeader.returns(cookie);
301 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
302 ctx.session = {
303 authenticatedIdentifier: 'user',
304 };
305 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
306 assert.strictEqual(result, true);
307 });
308 it('rejects insecure connection', async function () {
309 ctx.clientProtocol = 'http';
310 try {
311 await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
312 assert.fail(noExpectedException);
313 } catch (e) {
314 assert(e instanceof Errors.ResponseError);
315 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
316 }
317 });
318 it('ignores insecure connection if auth not required', async function () {
319 ctx.clientProtocol = 'http';
320 required = false;
321 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
322 assert.strictEqual(result, false);
323 });
324 it('redirects without any auth', async function () {
325 await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
326 assert(res.end.called);
327 assert(res.setHeader.called);
328 });
329 describe('convenience wrappers', function () {
330 describe('sessionRequiredLocal', function () {
331 it('accepts identifier', async function () {
332 req.getHeader.returns(cookie);
333 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
334 ctx.session = {
335 authenticatedIdentifier: 'user',
336 };
337 const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
338 assert.strictEqual(result, true);
339 });
340 it('redirects with profile', async function () {
341 req.getHeader.returns(cookie);
342 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
343 ctx.session = {
344 authenticatedProfile: 'user',
345 };
346 const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
347 assert.strictEqual(result, false);
348 assert(res.end.called);
349 assert(res.setHeader.called);
350 });
351 }); // sessionRequiredLocal
352 describe('sessionRequired', function () {
353 it('accepts identifier', async function () {
354 req.getHeader.returns(cookie);
355 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
356 ctx.session = {
357 authenticatedIdentifier: 'user',
358 };
359 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
360 assert.strictEqual(result, true);
361 });
362 it('accepts profile', async function () {
363 req.getHeader.returns(cookie);
364 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
365 ctx.session = {
366 authenticatedProfile: 'user',
367 };
368 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
369 assert.strictEqual(result, true);
370 });
371 it('rejects invalid', async function () {
372 req.getHeader.returns(cookie);
373 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
374 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
375 assert.strictEqual(result, false);
376 assert(res.end.called);
377 assert(res.setHeader.called);
378 });
379 it('covers insecure allowed', async function () {
380 authenticator.options.authenticator.secureAuthOnly = false;
381 req.getHeader.returns(cookie);
382 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
383 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
384 assert.strictEqual(result, false);
385 assert(res.end.called);
386 assert(res.setHeader.called);
387 });
388 }); // sessionRequired
389 describe('sessionOptionalLocal', function () {
390 it('rejects profile', async function () {
391 req.getHeader.returns(cookie);
392 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
393 ctx.session = {
394 authenticatedProfile: 'user',
395 };
396 const result = await authenticator.sessionOptionalLocal(req, res, ctx, loginPath);
397 assert.strictEqual(result, false);
398 assert(!res.end.called);
399 assert(!res.setHeader.called);
400 });
401 }); // sessionOptionalLocal
402 describe('sessionOptional', function () {
403 it('rejects invalid', async function () {
404 req.getHeader.returns(cookie);
405 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
406 const result = await authenticator.sessionOptional(req, res, ctx, loginPath);
407 assert.strictEqual(result, false);
408 assert(!res.end.called);
409 assert(!res.setHeader.called);
410 });
411 }); // sessionOptional
412 }); // convenience wrappers
413 }); // sessionCheck
414
415 describe('apiRequiredLocal', function () {
416 let req, res;
417 beforeEach(function () {
418 ctx = {};
419 req = {
420 getHeader: sinon.stub(),
421 };
422 res = {
423 end: sinon.stub(),
424 setHeader: sinon.stub(),
425 };
426 });
427 it('covers valid basic auth', async function () {
428 req.getHeader.returns('Basic Zm9vOmJhcg==');
429 sinon.stub(authenticator, 'sessionCheck').resolves(false);
430 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
431 const result = await authenticator.apiRequiredLocal(req, res, ctx);
432 assert.strictEqual(result, true);
433 assert(authenticator.isValidAuthorization.called);
434 assert(!authenticator.sessionCheck.called);
435 });
436 it('covers invalid basic auth', async function () {
437 req.getHeader.returns('Basic Zm9vOmJhcg==');
438 sinon.stub(authenticator, 'sessionCheck').resolves(false);
439 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
440 try {
441 await authenticator.apiRequiredLocal(req, res, ctx);
442 assert.fail(noExpectedException);
443 } catch (e) {
444 assert.strictEqual(e.statusCode, 401);
445 assert(!authenticator.sessionCheck.called);
446 assert(authenticator.isValidAuthorization.called);
447 }
448 });
449 it('covers missing basic auth, valid session', async function () {
450 req.getHeader.returns();
451 sinon.stub(authenticator, 'sessionCheck').resolves(true);
452 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
453 const result = await authenticator.apiRequiredLocal(req, res, ctx);
454 assert.strictEqual(result, true);
455 assert(!authenticator.isValidAuthorization.called);
456 assert(authenticator.sessionCheck.called);
457 });
458 it('covers missing basic auth, ignores session', async function () {
459 req.getHeader.returns();
460 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
461 try {
462 await authenticator.apiRequiredLocal(req, res, ctx, false);
463 assert.fail(noExpectedException);
464 } catch (e) {
465 assert.strictEqual(e.statusCode, 401);
466 assert(!authenticator.sessionCheck.called);
467 assert(!authenticator.isValidAuthorization.called);
468 assert(res.setHeader.called);
469 }
470 });
471 }); // apiRequiredLocal
472
473 }); // Authenticator