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