2 /* eslint-disable sonarjs/no-duplicate-string */
3 /* eslint-disable jsdoc/require-jsdoc */
6 const assert
= require('assert');
7 const sinon
= require('sinon');
8 const Authenticator
= require('../../lib/authenticator');
9 const stubLogger
= require('../stub-logger');
10 const stubDb
= require('../stub-db');
11 const Enum
= require('../../lib/enum');
12 const Config
= require('../stub-config');
14 describe('Authenticator', function () {
15 let authenticator
, credential
, ctx
, identifier
, password
, options
;
16 function _authMechanismRequired(a
, m
) {
17 if (!a
.authn
[m
]) { // eslint-disable-line security/detect-object-injection
22 beforeEach(function () {
23 options
= Config('test');
24 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
25 identifier
= 'username';
26 credential
= '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
28 password
= 'badPassword';
32 afterEach(function () {
36 it('covers no auth mechanisms', function () {
37 options
.authenticator
.authnEnabled
= [];
38 assert
.throws(() => new Authenticator(stubLogger
, stubDb
, options
), {
39 message: 'no authentication mechanisms available',
43 it('covers empty realm', function () {
44 options
.authenticator
.basicRealm
= undefined;
45 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
48 it('covers option defaults', function () {
49 delete options
.authenticator
.secureAuthOnly
;
50 delete options
.dingus
?.proxyPrefix
;
51 delete options
.authenticator
.forbiddenPAMIdentifiers
;
52 options
.authenticator
.authnEnabled
.push('flarpyauth');
53 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
56 describe('createIdentifier', function () {
58 beforeEach(function () {
60 credential
= 'badpassword';
62 it('covers success', async
function () {
63 const otpKey
= '1234567890123456789012';
64 await authenticator
.createIdentifier(dbCtx
, identifier
, credential
, otpKey
);
65 assert(authenticator
.db
.authenticationUpsert
.called
);
67 it('covers failure', async
function () {
68 const expected
= new Error('blah');
69 await authenticator
.db
.authenticationUpsert
.rejects(expected
);
70 // assert.rejects was not happy to handle this for some reason
72 await authenticator
.createIdentifier(dbCtx
, identifier
, credential
);
73 assert
.fail('no expecte exception');
75 assert
.deepStrictEqual(e
, expected
);
76 assert(authenticator
.db
.authenticationUpsert
.called
);
77 assert(authenticator
.logger
.error
.called
);
80 }); // createIdentifier
82 describe('updateCredential', function () {
83 let dbCtx
, newCredential
;
84 beforeEach(function () {
86 newCredential
= 'newpassword';
88 it('covers success', async
function () {
89 await authenticator
.updateCredential(dbCtx
, identifier
, newCredential
);
90 assert(authenticator
.db
.authenticationUpdateCredential
.called
);
91 assert(authenticator
.logger
.info
.called
);
93 it('covers failure', async
function () {
94 const expected
= new Error('foo');
95 authenticator
.db
.authenticationUpdateCredential
.rejects(expected
);
97 await authenticator
.updateCredential(dbCtx
, identifier
, newCredential
);
98 assert
.fail('no expected exception');
100 assert
.deepStrictEqual(e
, expected
);
101 assert(authenticator
.logger
.error
.called
);
103 // assert.rejects was not happy to handle this for some reason
105 }); // updateCredential
107 describe('_secureCredential', function () {
108 beforeEach(function () {
109 credential
= 'badpassword';
111 it('covers plain', async
function () {
112 const result
= await authenticator
._secureCredential(credential
, 'plain');
113 assert
.strictEqual(result
, '$plain$' + credential
);
115 it('covers default (argon2)', async
function () {
116 const result
= await authenticator
._secureCredential(credential
);
117 assert(result
.startsWith('$argon2'));
119 it('covers invalid authn', async
function () {
120 const authn
= 'bogus';
121 assert
.rejects(async () => await authenticator
._secureCredential(credential
, authn
), RangeError
);
123 }); // _secureCredential
125 describe('_validateAuthDataCredential', function () {
127 beforeEach(function () {
128 credential
= 'badpassword';
131 it('fails if not provided a credential', async
function () {
132 const result
= await authenticator
._validateAuthDataCredential(authData
, credential
);
133 assert
.strictEqual(result
, false);
135 it('covers plain', async
function () {
136 authData
.credential
= '$plain$badpassword';
137 const result
= await authenticator
._validateAuthDataCredential(authData
, credential
);
138 assert
.strictEqual(result
, true);
140 }); // _validateAuthDataCredential
142 describe('isValidBasic', function () {
143 it('succeeds', async
function () {
144 _authMechanismRequired(authenticator
, 'argon2');
145 authenticator
.db
.authenticationGet
.resolves({
149 const authString
= `${identifier}:${password}`;
150 const result
= await authenticator
.isValidBasic(authString
, ctx
);
151 assert
.strictEqual(result
, true);
152 assert
.strictEqual(ctx
.authenticationId
, identifier
);
154 it('fails', async
function () {
155 _authMechanismRequired(authenticator
, 'argon2');
156 authenticator
.db
.authenticationGet
.resolves({
160 const authString
= `${identifier}:wrongPassword}`;
161 const result
= await authenticator
.isValidBasic(authString
, ctx
);
162 assert
.strictEqual(result
, false);
163 assert
.strictEqual(ctx
.authenticationId
, undefined);
165 it('covers no entry', async
function() {
166 authenticator
.db
.authenticationGet
.resolves();
167 const authString
= `${identifier}:wrongPassword}`;
168 const result
= await authenticator
.isValidBasic(authString
, ctx
);
169 assert
.strictEqual(result
, false);
170 assert
.strictEqual(ctx
.authenticationId
, undefined);
172 it('covers unknown password hash', async
function () {
173 authenticator
.db
.authenticationGet
.resolves({
175 credential: '$other$kind_of_credential',
177 const authString
= `${identifier}:wrongPassword}`;
178 const result
= await authenticator
.isValidBasic(authString
, ctx
);
179 assert
.strictEqual(result
, false);
180 assert
.strictEqual(ctx
.authenticationId
, undefined);
184 describe('isValidIdentifierCredential', function () {
185 it('succeeds', async
function () {
186 _authMechanismRequired(authenticator
, 'argon2');
187 authenticator
.db
.authenticationGet
.resolves({
191 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
192 assert
.strictEqual(result
, true);
193 assert
.strictEqual(ctx
.authenticationId
, identifier
);
195 it('succeeds with OTP', async
function () {
196 const otpKey
= Buffer
.from('1234567890');
197 _authMechanismRequired(authenticator
, 'argon2');
198 authenticator
.db
.authenticationGet
.resolves({
203 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
204 assert
.strictEqual(result
, true);
205 assert
.strictEqual(ctx
.authenticationId
, identifier
);
206 assert
.deepStrictEqual(ctx
.otpKey
, otpKey
);
208 it('fails', async
function () {
209 _authMechanismRequired(authenticator
, 'argon2');
210 authenticator
.db
.authenticationGet
.resolves({
214 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
215 assert
.strictEqual(result
, false);
216 assert
.strictEqual(ctx
.authenticationId
, undefined);
218 it('covers no entry', async
function() {
219 authenticator
.db
.authenticationGet
.resolves();
220 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
221 assert
.strictEqual(result
, false);
222 assert
.strictEqual(ctx
.authenticationId
, undefined);
224 it('covers non-string credential', async
function () {
225 credential
= '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
226 const result
= await authenticator
.isValidIdentifierCredential(identifier
, undefined, ctx
);
227 assert
.strictEqual(result
, false);
228 assert
.strictEqual(ctx
.authenticationId
, undefined);
230 it('covers unknown password hash', async
function () {
231 authenticator
.db
.authenticationGet
.resolves({
233 credential: '$other$kind_of_credential',
235 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
236 assert
.strictEqual(result
, false);
237 assert
.strictEqual(ctx
.authenticationId
, undefined);
239 it('covers PAM', async
function () {
240 _authMechanismRequired(authenticator
, 'pam');
241 sinon
.stub(authenticator
, '_isValidPAMIdentifier').resolves(true);
242 authenticator
.db
.authenticationGet
.resolves({
246 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
247 assert
.strictEqual(result
, true);
248 assert
.strictEqual(ctx
.authenticationId
, identifier
);
250 }); // isValidIdentifierCredential
252 describe('_isValidPAMIdentifier', function () {
254 beforeEach(function () {
255 _authMechanismRequired(authenticator
, 'pam');
256 sinon
.stub(authenticator
.authn
.pam
, 'pamAuthenticatePromise');
261 it('covers success', async
function () {
262 authenticator
.authn
.pam
.pamAuthenticatePromise
.resolves(true);
263 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
264 assert
.strictEqual(result
, true);
266 it('covers failure', async
function () {
267 _authMechanismRequired(authenticator
, 'pam');
268 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(new authenticator
.authn
.pam
.PamError());
269 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
270 assert
.strictEqual(result
, false);
272 it('covers error', async
function () {
273 _authMechanismRequired(authenticator
, 'pam');
274 const expected
= new Error('blah');
275 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(expected
);
276 assert
.rejects(() => authenticator
._isValidPAMIdentifier(authData
, credential
), expected
);
278 it('covers forbidden', async
function () {
279 authData
.identifier
= 'root';
280 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
281 assert
.strictEqual(result
, false);
283 }); // _isValidPAMIdentifier
285 describe('isValidAuthorization', function () {
286 it('handles basic', async
function () {
287 const expected
= true;
288 const authorizationHeader
= 'basic Zm9vOmJhcg==';
289 sinon
.stub(authenticator
, 'isValidBasic').resolves(expected
);
290 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
291 assert
.strictEqual(result
, expected
);
293 it('handles other', async
function () {
294 const expected
= false;
295 const authorizationHeader
= 'bearer Zm9vOmJhcg==';
296 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
297 assert
.strictEqual(result
, expected
);
299 }); // isValidAuthorization
301 describe('requestBasic', function () {
302 it('covers', function () {
306 assert
.throws(() => authenticator
.requestBasic(res
), {
307 name: 'ResponseError',
308 statusCode: Enum
.ErrorResponse
.Unauthorized
.statusCode
,
313 describe('isValidCookieAuth', function () {
314 beforeEach(function () {
315 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
317 squeepSession: 'dummy',
321 it('covers identifier success', async
function () {
322 authenticator
.mysteryBox
.unpack
.resolves({
323 authenticatedIdentifier: 'identifier',
325 const result
= await authenticator
.isValidCookieAuth(ctx
);
326 assert
.strictEqual(result
, true);
328 it('covers profile success', async
function () {
329 authenticator
.mysteryBox
.unpack
.resolves({
330 authenticatedProfile: 'profile',
332 const result
= await authenticator
.isValidCookieAuth(ctx
);
333 assert
.strictEqual(result
, true);
335 it('covers missing cookie', async
function () {
336 delete ctx
.cookie
.squeepSession
;
337 const result
= await authenticator
.isValidCookieAuth(ctx
);
338 assert
.strictEqual(result
, false);
340 it('covers bad cookie', async
function () {
341 authenticator
.mysteryBox
.unpack
.rejects();
342 const result
= await authenticator
.isValidCookieAuth(ctx
);
343 assert
.strictEqual(result
, false);
345 it('covers broken session', async
function () {
346 authenticator
.mysteryBox
.unpack
.resolves({
349 const result
= await authenticator
.isValidCookieAuth(ctx
);
350 assert
.strictEqual(result
, false);
352 }); // isValidCookieAuth
354 describe('checkOTP', function () {
356 this.beforeEach(function () {
357 sinon
.stub(authenticator
.TOTP
.prototype, 'validate').returns(true);
359 key: '12345678901234567890123456789012',
365 it('covers valid OTP entry', function () {
366 const result
= authenticator
.checkOTP(state
, otp
);
367 assert
.strictEqual(result
, Enum
.OTPResult
.Valid
);
369 it('covers invalid OTP entry', function () {
370 authenticator
.TOTP
.prototype.validate
.returns(false);
371 const result
= authenticator
.checkOTP(state
, otp
);
372 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidSoftFail
);
374 it('covers invalid OTP entry, too many failures', function () {
376 authenticator
.TOTP
.prototype.validate
.returns(false);
377 const result
= authenticator
.checkOTP(state
, otp
);
378 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
380 it('covers invalid OTP entry', function () {
382 authenticator
.TOTP
.prototype.validate
.returns(false);
383 const result
= authenticator
.checkOTP(state
, otp
);
384 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
388 describe('updateOTPKey', function () {
390 beforeEach(function () {
392 otpKey
= 'CDBGB3U3B2ILECQORMINGGSZN7LXY565';
394 it('covers success', async
function () {
395 await authenticator
.updateOTPKey(dbCtx
, identifier
, otpKey
);
396 assert(authenticator
.db
.authenticationUpdateOTPKey
.called
);
398 it('covers failure', async
function () {
399 authenticator
.db
.authenticationUpdateOTPKey
.rejects();
400 assert
.rejects(authenticator
.updateOTPKey(dbCtx
, identifier
, otpKey
));
404 describe('sessionCheck', function () {
405 let req
, res
, loginPath
, required
, profilesAllowed
;
406 beforeEach(function () {
407 ctx
.clientProtocol
= 'https';
409 squeepSession: 'squeep_session_blob',
412 getHeader: sinon
.stub(),
416 setHeader: sinon
.stub(),
417 appendHeader: sinon
.stub(),
419 loginPath
= '/admin/login';
421 profilesAllowed
= true;
423 it('covers valid cookie session', async
function () {
424 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
426 authenticatedIdentifier: 'user',
428 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
429 assert
.strictEqual(result
, true);
431 it('covers valid insecure cookie session', async
function () {
432 authenticator
.secureAuthOnly
= false;
433 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
435 authenticatedIdentifier: 'user',
437 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
438 assert
.strictEqual(result
, true);
440 it('rejects insecure connection', async
function () {
441 ctx
.clientProtocol
= 'http';
442 assert
.rejects(() => authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
), {
443 name: 'ResponseError',
444 sttausCode: Enum
.ErrorResponse
.Forbidden
.statusCode
,
447 it('ignores insecure connection if auth not required', async
function () {
448 ctx
.clientProtocol
= 'http';
450 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
451 assert
.strictEqual(result
, false);
453 it('redirects without any auth', async
function () {
454 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
455 assert(res
.end
.called
);
456 assert(res
.setHeader
.called
);
458 describe('convenience wrappers', function () {
459 describe('sessionRequiredLocal', function () {
460 it('accepts identifier', async
function () {
461 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
463 authenticatedIdentifier: 'user',
465 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
466 assert
.strictEqual(result
, true);
468 it('redirects with profile', async
function () {
469 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
471 authenticatedProfile: 'user',
473 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
474 assert
.strictEqual(result
, false);
475 assert(res
.end
.called
);
476 assert(res
.setHeader
.called
);
478 }); // sessionRequiredLocal
479 describe('sessionRequired', function () {
480 it('accepts identifier', async
function () {
481 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
483 authenticatedIdentifier: 'user',
485 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
486 assert
.strictEqual(result
, true);
488 it('accepts profile', async
function () {
489 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
491 authenticatedProfile: 'user',
493 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
494 assert
.strictEqual(result
, true);
496 it('rejects invalid', async
function () {
497 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
498 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
499 assert
.strictEqual(result
, false);
500 assert(res
.end
.called
);
501 assert(res
.setHeader
.called
);
503 it('covers insecure allowed', async
function () {
504 authenticator
.options
.authenticator
.secureAuthOnly
= false;
505 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
506 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
507 assert
.strictEqual(result
, false);
508 assert(res
.end
.called
);
509 assert(res
.setHeader
.called
);
511 }); // sessionRequired
512 describe('sessionOptionalLocal', function () {
513 it('rejects profile', async
function () {
514 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
516 authenticatedProfile: 'user',
518 const result
= await authenticator
.sessionOptionalLocal(req
, res
, ctx
, loginPath
);
519 assert
.strictEqual(result
, false);
520 assert(!res
.end
.called
);
521 assert(!res
.setHeader
.called
);
523 }); // sessionOptionalLocal
524 describe('sessionOptional', function () {
525 it('rejects invalid', async
function () {
526 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
527 const result
= await authenticator
.sessionOptional(req
, res
, ctx
, loginPath
);
528 assert
.strictEqual(result
, false);
529 assert(!res
.end
.called
);
530 assert(!res
.setHeader
.called
);
532 }); // sessionOptional
533 }); // convenience wrappers
536 describe('apiRequiredLocal', function () {
538 beforeEach(function () {
541 getHeader: sinon
.stub(),
545 setHeader: sinon
.stub(),
548 it('covers valid basic auth', async
function () {
549 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
550 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
551 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
552 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
553 assert
.strictEqual(result
, true);
554 assert(authenticator
.isValidAuthorization
.called
);
555 assert(!authenticator
.sessionCheck
.called
);
557 it('covers invalid basic auth', async
function () {
558 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
559 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
560 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
561 assert
.rejects(() => authenticator
.apiRequiredLocal(req
, res
, ctx
), {
562 name: 'ResponseError',
565 assert(!authenticator
.sessionCheck
.called
);
566 assert(authenticator
.isValidAuthorization
.called
);
568 it('covers missing basic auth, valid session', async
function () {
569 req
.getHeader
.returns();
570 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
571 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
572 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
573 assert
.strictEqual(result
, true);
574 assert(!authenticator
.isValidAuthorization
.called
);
575 assert(authenticator
.sessionCheck
.called
);
577 it('covers missing basic auth, ignores session', async
function () {
578 req
.getHeader
.returns();
579 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
580 assert
.rejects(authenticator
.apiRequiredLocal(req
, res
, ctx
, false), {
581 name: 'ResponseError',
584 assert(!authenticator
.sessionCheck
.called
);
585 assert(!authenticator
.isValidAuthorization
.called
);
586 assert(res
.setHeader
.called
);
588 }); // apiRequiredLocal