test coverage for allowed insecure cookies
[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('covers valid insecure cookie session', async function () {
309 authenticator.secureAuthOnly = false;
310 req.getHeader.returns(cookie);
311 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
312 ctx.session = {
313 authenticatedIdentifier: 'user',
314 };
315 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
316 assert.strictEqual(result, true);
317 });
318 it('rejects insecure connection', async function () {
319 ctx.clientProtocol = 'http';
320 try {
321 await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
322 assert.fail(noExpectedException);
323 } catch (e) {
324 assert(e instanceof Errors.ResponseError);
325 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
326 }
327 });
328 it('ignores insecure connection if auth not required', async function () {
329 ctx.clientProtocol = 'http';
330 required = false;
331 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
332 assert.strictEqual(result, false);
333 });
334 it('redirects without any auth', async function () {
335 await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
336 assert(res.end.called);
337 assert(res.setHeader.called);
338 });
339 describe('convenience wrappers', function () {
340 describe('sessionRequiredLocal', function () {
341 it('accepts identifier', async function () {
342 req.getHeader.returns(cookie);
343 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
344 ctx.session = {
345 authenticatedIdentifier: 'user',
346 };
347 const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
348 assert.strictEqual(result, true);
349 });
350 it('redirects with profile', async function () {
351 req.getHeader.returns(cookie);
352 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
353 ctx.session = {
354 authenticatedProfile: 'user',
355 };
356 const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
357 assert.strictEqual(result, false);
358 assert(res.end.called);
359 assert(res.setHeader.called);
360 });
361 }); // sessionRequiredLocal
362 describe('sessionRequired', function () {
363 it('accepts identifier', async function () {
364 req.getHeader.returns(cookie);
365 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
366 ctx.session = {
367 authenticatedIdentifier: 'user',
368 };
369 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
370 assert.strictEqual(result, true);
371 });
372 it('accepts profile', async function () {
373 req.getHeader.returns(cookie);
374 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
375 ctx.session = {
376 authenticatedProfile: 'user',
377 };
378 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
379 assert.strictEqual(result, true);
380 });
381 it('rejects invalid', async function () {
382 req.getHeader.returns(cookie);
383 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
384 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
385 assert.strictEqual(result, false);
386 assert(res.end.called);
387 assert(res.setHeader.called);
388 });
389 it('covers insecure allowed', async function () {
390 authenticator.options.authenticator.secureAuthOnly = false;
391 req.getHeader.returns(cookie);
392 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
393 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
394 assert.strictEqual(result, false);
395 assert(res.end.called);
396 assert(res.setHeader.called);
397 });
398 }); // sessionRequired
399 describe('sessionOptionalLocal', function () {
400 it('rejects profile', async function () {
401 req.getHeader.returns(cookie);
402 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
403 ctx.session = {
404 authenticatedProfile: 'user',
405 };
406 const result = await authenticator.sessionOptionalLocal(req, res, ctx, loginPath);
407 assert.strictEqual(result, false);
408 assert(!res.end.called);
409 assert(!res.setHeader.called);
410 });
411 }); // sessionOptionalLocal
412 describe('sessionOptional', function () {
413 it('rejects invalid', async function () {
414 req.getHeader.returns(cookie);
415 sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
416 const result = await authenticator.sessionOptional(req, res, ctx, loginPath);
417 assert.strictEqual(result, false);
418 assert(!res.end.called);
419 assert(!res.setHeader.called);
420 });
421 }); // sessionOptional
422 }); // convenience wrappers
423 }); // sessionCheck
424
425 describe('apiRequiredLocal', function () {
426 let req, res;
427 beforeEach(function () {
428 ctx = {};
429 req = {
430 getHeader: sinon.stub(),
431 };
432 res = {
433 end: sinon.stub(),
434 setHeader: sinon.stub(),
435 };
436 });
437 it('covers valid basic auth', async function () {
438 req.getHeader.returns('Basic Zm9vOmJhcg==');
439 sinon.stub(authenticator, 'sessionCheck').resolves(false);
440 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
441 const result = await authenticator.apiRequiredLocal(req, res, ctx);
442 assert.strictEqual(result, true);
443 assert(authenticator.isValidAuthorization.called);
444 assert(!authenticator.sessionCheck.called);
445 });
446 it('covers invalid basic auth', async function () {
447 req.getHeader.returns('Basic Zm9vOmJhcg==');
448 sinon.stub(authenticator, 'sessionCheck').resolves(false);
449 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
450 try {
451 await authenticator.apiRequiredLocal(req, res, ctx);
452 assert.fail(noExpectedException);
453 } catch (e) {
454 assert.strictEqual(e.statusCode, 401);
455 assert(!authenticator.sessionCheck.called);
456 assert(authenticator.isValidAuthorization.called);
457 }
458 });
459 it('covers missing basic auth, valid session', async function () {
460 req.getHeader.returns();
461 sinon.stub(authenticator, 'sessionCheck').resolves(true);
462 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
463 const result = await authenticator.apiRequiredLocal(req, res, ctx);
464 assert.strictEqual(result, true);
465 assert(!authenticator.isValidAuthorization.called);
466 assert(authenticator.sessionCheck.called);
467 });
468 it('covers missing basic auth, ignores session', async function () {
469 req.getHeader.returns();
470 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
471 try {
472 await authenticator.apiRequiredLocal(req, res, ctx, false);
473 assert.fail(noExpectedException);
474 } catch (e) {
475 assert.strictEqual(e.statusCode, 401);
476 assert(!authenticator.sessionCheck.called);
477 assert(!authenticator.isValidAuthorization.called);
478 assert(res.setHeader.called);
479 }
480 });
481 }); // apiRequiredLocal
482
483 }); // Authenticator