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';
29 afterEach(function () {
33 it('covers no auth mechanisms', function () {
34 options
.authenticator
.authnEnabled
= [];
35 assert
.throws(() => new Authenticator(stubLogger
, stubDb
, options
), {
36 message: 'no authentication mechanisms available',
40 it('covers empty realm', function () {
41 options
.authenticator
.basicRealm
= undefined;
42 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
45 describe('isValidBasic', function () {
46 it('succeeds', async
function () {
47 _authMechanismRequired(authenticator
, 'argon2');
48 authenticator
.db
.authenticationGet
.resolves({
52 const authString
= `${identifier}:${password}`;
53 const result
= await authenticator
.isValidBasic(authString
, ctx
);
54 assert
.strictEqual(result
, true);
55 assert
.strictEqual(ctx
.authenticationId
, identifier
);
57 it('fails', async
function () {
58 _authMechanismRequired(authenticator
, 'argon2');
59 authenticator
.db
.authenticationGet
.resolves({
63 const authString
= `${identifier}:wrongPassword}`;
64 const result
= await authenticator
.isValidBasic(authString
, ctx
);
65 assert
.strictEqual(result
, false);
66 assert
.strictEqual(ctx
.authenticationId
, undefined);
68 it('covers no entry', async
function() {
69 authenticator
.db
.authenticationGet
.resolves();
70 const authString
= `${identifier}:wrongPassword}`;
71 const result
= await authenticator
.isValidBasic(authString
, ctx
);
72 assert
.strictEqual(result
, false);
73 assert
.strictEqual(ctx
.authenticationId
, undefined);
75 it('covers unknown password hash', async
function () {
76 authenticator
.db
.authenticationGet
.resolves({
78 credential: '$other$kind_of_credential',
80 const authString
= `${identifier}:wrongPassword}`;
81 const result
= await authenticator
.isValidBasic(authString
, ctx
);
82 assert
.strictEqual(result
, false);
83 assert
.strictEqual(ctx
.authenticationId
, undefined);
87 describe('isValidIdentifierCredential', function () {
88 it('succeeds', async
function () {
89 _authMechanismRequired(authenticator
, 'argon2');
90 authenticator
.db
.authenticationGet
.resolves({
94 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
95 assert
.strictEqual(result
, true);
96 assert
.strictEqual(ctx
.authenticationId
, identifier
);
98 it('fails', async
function () {
99 _authMechanismRequired(authenticator
, 'argon2');
100 authenticator
.db
.authenticationGet
.resolves({
104 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
105 assert
.strictEqual(result
, false);
106 assert
.strictEqual(ctx
.authenticationId
, undefined);
108 it('covers no entry', async
function() {
109 authenticator
.db
.authenticationGet
.resolves();
110 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
111 assert
.strictEqual(result
, false);
112 assert
.strictEqual(ctx
.authenticationId
, undefined);
114 it('covers non-string credential', async
function () {
115 credential
= '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
116 const result
= await authenticator
.isValidIdentifierCredential(identifier
, undefined, ctx
);
117 assert
.strictEqual(result
, false);
118 assert
.strictEqual(ctx
.authenticationId
, undefined);
120 it('covers unknown password hash', async
function () {
121 authenticator
.db
.authenticationGet
.resolves({
123 credential: '$other$kind_of_credential',
125 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
126 assert
.strictEqual(result
, false);
127 assert
.strictEqual(ctx
.authenticationId
, undefined);
129 it('covers PAM', async
function () {
130 _authMechanismRequired(authenticator
, 'pam');
131 sinon
.stub(authenticator
, '_isValidPAMIdentifier').resolves(true);
132 authenticator
.db
.authenticationGet
.resolves({
136 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
137 assert
.strictEqual(result
, true);
138 assert
.strictEqual(ctx
.authenticationId
, identifier
);
140 it('covers debug', async
function () {
141 authenticator
.authnEnabled
= ['DEBUG_ANY'];
142 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
143 assert
.strictEqual(result
, true);
144 assert
.strictEqual(ctx
.authenticationId
, identifier
);
146 }); // isValidIdentifierCredential
148 describe('_isValidPAMIdentifier', function () {
149 beforeEach(function () {
150 _authMechanismRequired(authenticator
, 'pam');
151 sinon
.stub(authenticator
.authn
.pam
, 'pamAuthenticatePromise');
153 it('covers success', async
function () {
154 authenticator
.authn
.pam
.pamAuthenticatePromise
.resolves(true);
155 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
156 assert
.strictEqual(result
, true);
158 it('covers failure', async
function () {
159 _authMechanismRequired(authenticator
, 'pam');
160 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(new authenticator
.authn
.pam
.PamError());
161 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
162 assert
.strictEqual(result
, false);
164 it('covers error', async
function () {
165 _authMechanismRequired(authenticator
, 'pam');
166 const expected
= new Error('blah');
167 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(expected
);
168 assert
.rejects(() => authenticator
._isValidPAMIdentifier(identifier
, credential
), expected
);
170 it('covers forbidden', async
function () {
172 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
173 assert
.strictEqual(result
, false);
175 }); // _isValidPAMIdentifier
177 describe('_cookieParse', function () {
178 it('covers empty', function () {
180 const result
= Authenticator
._cookieParse();
181 assert
.deepStrictEqual(result
, expected
);
183 it('covers non variable', function () {
184 const cookie
= 'foo';
188 const result
= Authenticator
._cookieParse(cookie
);
189 assert
.deepStrictEqual(result
, expected
);
191 it('parses cookie', function () {
192 const cookie
= 'foo=bar; baz="quux"';
197 const result
= Authenticator
._cookieParse(cookie
);
198 assert
.deepStrictEqual(result
, expected
);
202 describe('isValidAuthorization', function () {
203 it('handles basic', async
function () {
204 const expected
= true;
205 const authorizationHeader
= 'basic Zm9vOmJhcg==';
206 sinon
.stub(authenticator
, 'isValidBasic').resolves(expected
);
207 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
208 assert
.strictEqual(result
, expected
);
210 it('handles other', async
function () {
211 const expected
= false;
212 const authorizationHeader
= 'bearer Zm9vOmJhcg==';
213 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
214 assert
.strictEqual(result
, expected
);
216 }); // isValidAuthorization
218 describe('requestBasic', function () {
219 it('covers', function () {
223 assert
.throws(() => authenticator
.requestBasic(res
), {
224 name: 'ResponseError',
225 statusCode: Enum
.ErrorResponse
.Unauthorized
.statusCode
,
230 describe('isValidCookieAuth', function () {
232 beforeEach(function () {
233 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
234 cookie
= 'squeepSession=dummy';
236 it('covers identifier success', async
function () {
237 authenticator
.mysteryBox
.unpack
.resolves({
238 authenticatedIdentifier: 'identifier',
240 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
241 assert
.strictEqual(result
, true);
243 it('covers profile success', async
function () {
244 authenticator
.mysteryBox
.unpack
.resolves({
245 authenticatedProfile: 'profile',
247 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
248 assert
.strictEqual(result
, true);
250 it('covers missing cookie', async
function () {
251 cookie
= 'wrong=cookie';
252 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
253 assert
.strictEqual(result
, false);
255 it('covers bad cookie', async
function () {
256 authenticator
.mysteryBox
.unpack
.rejects();
257 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
258 assert
.strictEqual(result
, false);
260 it('covers broken session', async
function () {
261 authenticator
.mysteryBox
.unpack
.resolves({
264 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
265 assert
.strictEqual(result
, false);
267 }); // isValidCookieAuth
269 describe('checkOTP', function () {
271 this.beforeEach(function () {
272 sinon
.stub(authenticator
.TOTP
.prototype, 'validate').returns(true);
274 key: Buffer
.from('12345678901234567890'),
280 it('covers valid OTP entry', function () {
281 const result
= authenticator
.checkOTP(state
, otp
);
282 assert
.strictEqual(result
, Enum
.OTPResult
.Valid
);
284 it('covers invalid OTP entry', function () {
285 authenticator
.TOTP
.prototype.validate
.returns(false);
286 const result
= authenticator
.checkOTP(state
, otp
);
287 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidSoftFail
);
289 it('covers invalid OTP entry, too many failures', function () {
291 authenticator
.TOTP
.prototype.validate
.returns(false);
292 const result
= authenticator
.checkOTP(state
, otp
);
293 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
295 it('covers invalid OTP entry', function () {
297 authenticator
.TOTP
.prototype.validate
.returns(false);
298 const result
= authenticator
.checkOTP(state
, otp
);
299 assert
.strictEqual(result
, Enum
.OTPResult
.InvalidHardFail
);
303 describe('sessionCheck', function () {
304 let cookie
, req
, res
, loginPath
, required
, profilesAllowed
;
305 beforeEach(function () {
306 cookie
= 'squeepSession=sessionCookie';
307 ctx
.clientProtocol
= 'https';
309 getHeader: sinon
.stub(),
313 setHeader: sinon
.stub(),
315 loginPath
= '/admin/login';
317 profilesAllowed
= true;
319 it('covers valid cookie session', async
function () {
320 req
.getHeader
.returns(cookie
);
321 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
323 authenticatedIdentifier: 'user',
325 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
326 assert
.strictEqual(result
, true);
328 it('covers valid insecure cookie session', async
function () {
329 authenticator
.secureAuthOnly
= false;
330 req
.getHeader
.returns(cookie
);
331 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
333 authenticatedIdentifier: 'user',
335 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
336 assert
.strictEqual(result
, true);
338 it('rejects insecure connection', async
function () {
339 ctx
.clientProtocol
= 'http';
340 assert
.rejects(() => authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
), {
341 name: 'ResponseError',
342 sttausCode: Enum
.ErrorResponse
.Forbidden
.statusCode
,
345 it('ignores insecure connection if auth not required', async
function () {
346 ctx
.clientProtocol
= 'http';
348 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
349 assert
.strictEqual(result
, false);
351 it('redirects without any auth', async
function () {
352 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
353 assert(res
.end
.called
);
354 assert(res
.setHeader
.called
);
356 describe('convenience wrappers', function () {
357 describe('sessionRequiredLocal', function () {
358 it('accepts identifier', async
function () {
359 req
.getHeader
.returns(cookie
);
360 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
362 authenticatedIdentifier: 'user',
364 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
365 assert
.strictEqual(result
, true);
367 it('redirects with profile', async
function () {
368 req
.getHeader
.returns(cookie
);
369 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
371 authenticatedProfile: 'user',
373 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
374 assert
.strictEqual(result
, false);
375 assert(res
.end
.called
);
376 assert(res
.setHeader
.called
);
378 }); // sessionRequiredLocal
379 describe('sessionRequired', function () {
380 it('accepts identifier', async
function () {
381 req
.getHeader
.returns(cookie
);
382 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
384 authenticatedIdentifier: 'user',
386 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
387 assert
.strictEqual(result
, true);
389 it('accepts profile', async
function () {
390 req
.getHeader
.returns(cookie
);
391 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
393 authenticatedProfile: 'user',
395 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
396 assert
.strictEqual(result
, true);
398 it('rejects invalid', async
function () {
399 req
.getHeader
.returns(cookie
);
400 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
401 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
402 assert
.strictEqual(result
, false);
403 assert(res
.end
.called
);
404 assert(res
.setHeader
.called
);
406 it('covers insecure allowed', async
function () {
407 authenticator
.options
.authenticator
.secureAuthOnly
= false;
408 req
.getHeader
.returns(cookie
);
409 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
410 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
411 assert
.strictEqual(result
, false);
412 assert(res
.end
.called
);
413 assert(res
.setHeader
.called
);
415 }); // sessionRequired
416 describe('sessionOptionalLocal', function () {
417 it('rejects profile', async
function () {
418 req
.getHeader
.returns(cookie
);
419 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
421 authenticatedProfile: 'user',
423 const result
= await authenticator
.sessionOptionalLocal(req
, res
, ctx
, loginPath
);
424 assert
.strictEqual(result
, false);
425 assert(!res
.end
.called
);
426 assert(!res
.setHeader
.called
);
428 }); // sessionOptionalLocal
429 describe('sessionOptional', function () {
430 it('rejects invalid', async
function () {
431 req
.getHeader
.returns(cookie
);
432 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
433 const result
= await authenticator
.sessionOptional(req
, res
, ctx
, loginPath
);
434 assert
.strictEqual(result
, false);
435 assert(!res
.end
.called
);
436 assert(!res
.setHeader
.called
);
438 }); // sessionOptional
439 }); // convenience wrappers
442 describe('apiRequiredLocal', function () {
444 beforeEach(function () {
447 getHeader: sinon
.stub(),
451 setHeader: sinon
.stub(),
454 it('covers valid basic auth', async
function () {
455 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
456 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
457 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
458 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
459 assert
.strictEqual(result
, true);
460 assert(authenticator
.isValidAuthorization
.called
);
461 assert(!authenticator
.sessionCheck
.called
);
463 it('covers invalid basic auth', async
function () {
464 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
465 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
466 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
467 assert
.rejects(() => authenticator
.apiRequiredLocal(req
, res
, ctx
), {
468 name: 'ResponseError',
471 assert(!authenticator
.sessionCheck
.called
);
472 assert(authenticator
.isValidAuthorization
.called
);
474 it('covers missing basic auth, valid session', async
function () {
475 req
.getHeader
.returns();
476 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
477 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
478 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
479 assert
.strictEqual(result
, true);
480 assert(!authenticator
.isValidAuthorization
.called
);
481 assert(authenticator
.sessionCheck
.called
);
483 it('covers missing basic auth, ignores session', async
function () {
484 req
.getHeader
.returns();
485 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
486 assert
.rejects(authenticator
.apiRequiredLocal(req
, res
, ctx
, false), {
487 name: 'ResponseError',
490 assert(!authenticator
.sessionCheck
.called
);
491 assert(!authenticator
.isValidAuthorization
.called
);
492 assert(res
.setHeader
.called
);
494 }); // apiRequiredLocal