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 Enum
= require('../../lib/enum');
10 const Config
= require('../stub-config');
12 describe('Authenticator', function () {
13 let authenticator
, credential
, ctx
, identifier
, password
, options
;
14 function _authMechanismRequired(a
, m
) {
15 if (!a
.authn
[m
]) { // eslint-disable-line security/detect-object-injection
20 beforeEach(function () {
21 options
= Config('test');
22 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
23 identifier
= 'username';
24 credential
= '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
26 password
= 'badPassword';
30 afterEach(function () {
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',
41 it('covers empty realm', function () {
42 options
.authenticator
.basicRealm
= undefined;
43 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
46 it('covers option defaults', function () {
47 delete options
.authenticator
.secureAuthOnly
;
48 delete options
.dingus
?.proxyPrefix
;
49 delete options
.authenticator
.forbiddenPAMIdentifiers
;
50 options
.authenticator
.authnEnabled
.push('flarpyauth');
51 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
54 describe('createIdentifier', function () {
56 beforeEach(function () {
58 credential
= 'badpassword';
60 it('covers success', async
function () {
61 const otpKey
= '1234567890123456789012';
62 await authenticator
.createIdentifier(dbCtx
, identifier
, credential
, otpKey
);
63 assert(authenticator
.db
.authenticationInsertIdentifier
.called
);
65 it('covers failure', async
function () {
66 const expected
= new Error('blah');
67 await authenticator
.db
.authenticationInsertIdentifier
.rejects(expected
);
68 // assert.rejects was not happy to handle this for some reason
70 await authenticator
.createIdentifier(dbCtx
, identifier
, credential
);
71 assert
.fail('no expecte exception');
73 assert
.deepStrictEqual(e
, expected
);
74 assert(authenticator
.db
.authenticationInsertIdentifier
.called
);
75 assert(authenticator
.logger
.error
.called
);
78 }); // createIdentifier
80 describe('updateCredential', function () {
81 let dbCtx
, newCredential
;
82 beforeEach(function () {
84 newCredential
= 'newpassword';
86 it('covers success', async
function () {
87 await authenticator
.updateCredential(dbCtx
, identifier
, newCredential
);
88 assert(authenticator
.db
.authenticationUpdateCredential
.called
);
89 assert(authenticator
.logger
.info
.called
);
91 it('covers failure', async
function () {
92 const expected
= new Error('foo');
93 authenticator
.db
.authenticationUpdateCredential
.rejects(expected
);
95 await authenticator
.updateCredential(dbCtx
, identifier
, newCredential
);
96 assert
.fail('no expected exception');
98 assert
.deepStrictEqual(e
, expected
);
99 assert(authenticator
.logger
.error
.called
);
101 // assert.rejects was not happy to handle this for some reason
103 }); // updateCredential
105 describe('_secureCredential', function () {
106 beforeEach(function () {
107 credential
= 'badpassword';
109 it('covers plain', async
function () {
110 const result
= await authenticator
._secureCredential(credential
, 'plain');
111 assert
.strictEqual(result
, '$plain$' + credential
);
113 it('covers default (argon2)', async
function () {
114 const result
= await authenticator
._secureCredential(credential
);
115 assert(result
.startsWith('$argon2'));
117 it('covers invalid authn', async
function () {
118 const authn
= 'bogus';
119 assert
.rejects(async () => await authenticator
._secureCredential(credential
, authn
), RangeError
);
121 }); // _secureCredential
123 describe('_validateAuthDataCredential', function () {
125 beforeEach(function () {
126 credential
= 'badpassword';
129 it('fails if not provided a credential', async
function () {
130 const result
= await authenticator
._validateAuthDataCredential(authData
, credential
);
131 assert
.strictEqual(result
, false);
133 it('covers plain', async
function () {
134 authData
.credential
= '$plain$badpassword';
135 const result
= await authenticator
._validateAuthDataCredential(authData
, credential
);
136 assert
.strictEqual(result
, true);
138 }); // _validateAuthDataCredential
140 describe('isValidBasic', function () {
141 it('succeeds', async
function () {
142 _authMechanismRequired(authenticator
, 'argon2');
143 authenticator
.db
.authenticationGet
.resolves({
147 const authString
= `${identifier}:${password}`;
148 const result
= await authenticator
.isValidBasic(authString
, ctx
);
149 assert
.strictEqual(result
, true);
150 assert
.strictEqual(ctx
.authenticationId
, identifier
);
152 it('fails', async
function () {
153 _authMechanismRequired(authenticator
, 'argon2');
154 authenticator
.db
.authenticationGet
.resolves({
158 const authString
= `${identifier}:wrongPassword}`;
159 const result
= await authenticator
.isValidBasic(authString
, ctx
);
160 assert
.strictEqual(result
, false);
161 assert
.strictEqual(ctx
.authenticationId
, undefined);
163 it('covers no entry', async
function() {
164 authenticator
.db
.authenticationGet
.resolves();
165 const authString
= `${identifier}:wrongPassword}`;
166 const result
= await authenticator
.isValidBasic(authString
, ctx
);
167 assert
.strictEqual(result
, false);
168 assert
.strictEqual(ctx
.authenticationId
, undefined);
170 it('covers unknown password hash', async
function () {
171 authenticator
.db
.authenticationGet
.resolves({
173 credential: '$other$kind_of_credential',
175 const authString
= `${identifier}:wrongPassword}`;
176 const result
= await authenticator
.isValidBasic(authString
, ctx
);
177 assert
.strictEqual(result
, false);
178 assert
.strictEqual(ctx
.authenticationId
, undefined);
182 describe('isValidIdentifierCredential', function () {
183 it('succeeds', async
function () {
184 _authMechanismRequired(authenticator
, 'argon2');
185 authenticator
.db
.authenticationGet
.resolves({
189 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
190 assert
.strictEqual(result
, true);
191 assert
.strictEqual(ctx
.authenticationId
, identifier
);
193 it('succeeds with OTP', async
function () {
194 const otpKey
= Buffer
.from('1234567890');
195 _authMechanismRequired(authenticator
, 'argon2');
196 authenticator
.db
.authenticationGet
.resolves({
201 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
202 assert
.strictEqual(result
, true);
203 assert
.strictEqual(ctx
.authenticationId
, identifier
);
204 assert
.deepStrictEqual(ctx
.otpKey
, otpKey
);
206 it('fails', async
function () {
207 _authMechanismRequired(authenticator
, 'argon2');
208 authenticator
.db
.authenticationGet
.resolves({
212 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
213 assert
.strictEqual(result
, false);
214 assert
.strictEqual(ctx
.authenticationId
, undefined);
216 it('covers no entry', async
function() {
217 authenticator
.db
.authenticationGet
.resolves();
218 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
219 assert
.strictEqual(result
, false);
220 assert
.strictEqual(ctx
.authenticationId
, undefined);
222 it('covers non-string credential', async
function () {
223 credential
= '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
224 const result
= await authenticator
.isValidIdentifierCredential(identifier
, undefined, ctx
);
225 assert
.strictEqual(result
, false);
226 assert
.strictEqual(ctx
.authenticationId
, undefined);
228 it('covers unknown password hash', async
function () {
229 authenticator
.db
.authenticationGet
.resolves({
231 credential: '$other$kind_of_credential',
233 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
234 assert
.strictEqual(result
, false);
235 assert
.strictEqual(ctx
.authenticationId
, undefined);
237 it('covers PAM', async
function () {
238 _authMechanismRequired(authenticator
, 'pam');
239 sinon
.stub(authenticator
, '_isValidPAMIdentifier').resolves(true);
240 authenticator
.db
.authenticationGet
.resolves({
244 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
245 assert
.strictEqual(result
, true);
246 assert
.strictEqual(ctx
.authenticationId
, identifier
);
248 }); // isValidIdentifierCredential
250 describe('_isValidPAMIdentifier', function () {
252 beforeEach(function () {
253 _authMechanismRequired(authenticator
, 'pam');
254 sinon
.stub(authenticator
.authn
.pam
, 'pamAuthenticatePromise');
259 it('covers success', async
function () {
260 authenticator
.authn
.pam
.pamAuthenticatePromise
.resolves(true);
261 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
262 assert
.strictEqual(result
, true);
264 it('covers failure', async
function () {
265 _authMechanismRequired(authenticator
, 'pam');
266 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(new authenticator
.authn
.pam
.PamError());
267 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
268 assert
.strictEqual(result
, false);
270 it('covers error', async
function () {
271 _authMechanismRequired(authenticator
, 'pam');
272 const expected
= new Error('blah');
273 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(expected
);
274 assert
.rejects(() => authenticator
._isValidPAMIdentifier(authData
, credential
), expected
);
276 it('covers forbidden', async
function () {
277 authData
.identifier
= 'root';
278 const result
= await authenticator
._isValidPAMIdentifier(authData
, credential
);
279 assert
.strictEqual(result
, false);
281 }); // _isValidPAMIdentifier
283 describe('isValidAuthorization', function () {
284 it('handles basic', async
function () {
285 const expected
= true;
286 const authorizationHeader
= 'basic Zm9vOmJhcg==';
287 sinon
.stub(authenticator
, 'isValidBasic').resolves(expected
);
288 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
289 assert
.strictEqual(result
, expected
);
291 it('handles other', async
function () {
292 const expected
= false;
293 const authorizationHeader
= 'bearer Zm9vOmJhcg==';
294 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
295 assert
.strictEqual(result
, expected
);
297 }); // isValidAuthorization
299 describe('requestBasic', function () {
300 it('covers', function () {
304 assert
.throws(() => authenticator
.requestBasic(res
), {
305 name: 'ResponseError',
306 statusCode: Enum
.ErrorResponse
.Unauthorized
.statusCode
,
311 describe('isValidCookieAuth', function () {
312 beforeEach(function () {
313 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
315 squeepSession: 'dummy',
319 it('covers identifier success', async
function () {
320 authenticator
.mysteryBox
.unpack
.resolves({
321 authenticatedIdentifier: 'identifier',
323 const result
= await authenticator
.isValidCookieAuth(ctx
);
324 assert
.strictEqual(result
, true);
326 it('covers profile success', async
function () {
327 authenticator
.mysteryBox
.unpack
.resolves({
328 authenticatedProfile: 'profile',
330 const result
= await authenticator
.isValidCookieAuth(ctx
);
331 assert
.strictEqual(result
, true);
333 it('covers missing cookie', async
function () {
334 delete ctx
.cookie
.squeepSession
;
335 const result
= await authenticator
.isValidCookieAuth(ctx
);
336 assert
.strictEqual(result
, false);
338 it('covers bad cookie', async
function () {
339 authenticator
.mysteryBox
.unpack
.rejects();
340 const result
= await authenticator
.isValidCookieAuth(ctx
);
341 assert
.strictEqual(result
, false);
343 it('covers broken session', async
function () {
344 authenticator
.mysteryBox
.unpack
.resolves({
347 const result
= await authenticator
.isValidCookieAuth(ctx
);
348 assert
.strictEqual(result
, false);
350 }); // isValidCookieAuth
352 describe('checkOTP', function () {
354 this.beforeEach(function () {
355 sinon
.stub(authenticator
.TOTP
.prototype, 'validate').returns(true);
357 key: '12345678901234567890123456789012',
363 it('covers valid OTP entry', function () {
364 const result
= authenticator
.checkOTP(state
, otp
);
365 assert
.strictEqual(result
, Enum
.OTPResult
.Valid
);
367 it('covers invalid OTP entry', function () {
368 authenticator
.TOTP
.prototype.validate
.returns(false);
369 const result
= authenticator
.checkOTP(state
, otp
);
370 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidSoftFail
);
372 it('covers invalid OTP entry, too many failures', function () {
374 authenticator
.TOTP
.prototype.validate
.returns(false);
375 const result
= authenticator
.checkOTP(state
, otp
);
376 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
378 it('covers invalid OTP entry', function () {
380 authenticator
.TOTP
.prototype.validate
.returns(false);
381 const result
= authenticator
.checkOTP(state
, otp
);
382 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
386 describe('sessionCheck', function () {
387 let req
, res
, loginPath
, required
, profilesAllowed
;
388 beforeEach(function () {
389 ctx
.clientProtocol
= 'https';
391 squeepSession: 'squeep_session_blob',
394 getHeader: sinon
.stub(),
398 setHeader: sinon
.stub(),
399 appendHeader: sinon
.stub(),
401 loginPath
= '/admin/login';
403 profilesAllowed
= true;
405 it('covers valid cookie session', async
function () {
406 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
408 authenticatedIdentifier: 'user',
410 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
411 assert
.strictEqual(result
, true);
413 it('covers valid insecure cookie session', async
function () {
414 authenticator
.secureAuthOnly
= false;
415 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
417 authenticatedIdentifier: 'user',
419 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
420 assert
.strictEqual(result
, true);
422 it('rejects insecure connection', async
function () {
423 ctx
.clientProtocol
= 'http';
424 assert
.rejects(() => authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
), {
425 name: 'ResponseError',
426 sttausCode: Enum
.ErrorResponse
.Forbidden
.statusCode
,
429 it('ignores insecure connection if auth not required', async
function () {
430 ctx
.clientProtocol
= 'http';
432 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
433 assert
.strictEqual(result
, false);
435 it('redirects without any auth', async
function () {
436 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
437 assert(res
.end
.called
);
438 assert(res
.setHeader
.called
);
440 describe('convenience wrappers', function () {
441 describe('sessionRequiredLocal', function () {
442 it('accepts identifier', async
function () {
443 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
445 authenticatedIdentifier: 'user',
447 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
448 assert
.strictEqual(result
, true);
450 it('redirects with profile', async
function () {
451 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
453 authenticatedProfile: 'user',
455 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
456 assert
.strictEqual(result
, false);
457 assert(res
.end
.called
);
458 assert(res
.setHeader
.called
);
460 }); // sessionRequiredLocal
461 describe('sessionRequired', function () {
462 it('accepts identifier', async
function () {
463 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
465 authenticatedIdentifier: 'user',
467 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
468 assert
.strictEqual(result
, true);
470 it('accepts profile', async
function () {
471 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
473 authenticatedProfile: 'user',
475 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
476 assert
.strictEqual(result
, true);
478 it('rejects invalid', async
function () {
479 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
480 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
481 assert
.strictEqual(result
, false);
482 assert(res
.end
.called
);
483 assert(res
.setHeader
.called
);
485 it('covers insecure allowed', async
function () {
486 authenticator
.options
.authenticator
.secureAuthOnly
= false;
487 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
488 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
489 assert
.strictEqual(result
, false);
490 assert(res
.end
.called
);
491 assert(res
.setHeader
.called
);
493 }); // sessionRequired
494 describe('sessionOptionalLocal', function () {
495 it('rejects profile', async
function () {
496 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
498 authenticatedProfile: 'user',
500 const result
= await authenticator
.sessionOptionalLocal(req
, res
, ctx
, loginPath
);
501 assert
.strictEqual(result
, false);
502 assert(!res
.end
.called
);
503 assert(!res
.setHeader
.called
);
505 }); // sessionOptionalLocal
506 describe('sessionOptional', function () {
507 it('rejects invalid', async
function () {
508 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
509 const result
= await authenticator
.sessionOptional(req
, res
, ctx
, loginPath
);
510 assert
.strictEqual(result
, false);
511 assert(!res
.end
.called
);
512 assert(!res
.setHeader
.called
);
514 }); // sessionOptional
515 }); // convenience wrappers
518 describe('apiRequiredLocal', function () {
520 beforeEach(function () {
523 getHeader: sinon
.stub(),
527 setHeader: sinon
.stub(),
530 it('covers valid basic auth', async
function () {
531 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
532 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
533 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
534 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
535 assert
.strictEqual(result
, true);
536 assert(authenticator
.isValidAuthorization
.called
);
537 assert(!authenticator
.sessionCheck
.called
);
539 it('covers invalid basic auth', async
function () {
540 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
541 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
542 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
543 assert
.rejects(() => authenticator
.apiRequiredLocal(req
, res
, ctx
), {
544 name: 'ResponseError',
547 assert(!authenticator
.sessionCheck
.called
);
548 assert(authenticator
.isValidAuthorization
.called
);
550 it('covers missing basic auth, valid session', async
function () {
551 req
.getHeader
.returns();
552 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
553 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
554 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
555 assert
.strictEqual(result
, true);
556 assert(!authenticator
.isValidAuthorization
.called
);
557 assert(authenticator
.sessionCheck
.called
);
559 it('covers missing basic auth, ignores session', async
function () {
560 req
.getHeader
.returns();
561 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
562 assert
.rejects(authenticator
.apiRequiredLocal(req
, res
, ctx
, false), {
563 name: 'ResponseError',
566 assert(!authenticator
.sessionCheck
.called
);
567 assert(!authenticator
.isValidAuthorization
.called
);
568 assert(res
.setHeader
.called
);
570 }); // apiRequiredLocal