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');
13 const noExpectedException
= 'did not receive expected exception';
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
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';
29 password
= 'badPassword';
32 afterEach(function () {
36 it('covers no auth mechanisms', function () {
37 options
.authenticator
.authnEnabled
= [];
39 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
40 assert
.fail(noExpectedException
);
42 assert
.strictEqual(e
.message
, 'no authentication mechanisms available');
46 it('covers empty realm', function () {
47 options
.authenticator
.basicRealm
= undefined;
48 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
51 describe('isValidBasic', function () {
52 it('succeeds', async
function () {
53 _authMechanismRequired(authenticator
, 'argon2');
54 authenticator
.db
.authenticationGet
.resolves({
58 const authString
= `${identifier}:${password}`;
59 const result
= await authenticator
.isValidBasic(authString
, ctx
);
60 assert
.strictEqual(result
, true);
61 assert
.strictEqual(ctx
.authenticationId
, identifier
);
63 it('fails', async
function () {
64 _authMechanismRequired(authenticator
, 'argon2');
65 authenticator
.db
.authenticationGet
.resolves({
69 const authString
= `${identifier}:wrongPassword}`;
70 const result
= await authenticator
.isValidBasic(authString
, ctx
);
71 assert
.strictEqual(result
, false);
72 assert
.strictEqual(ctx
.authenticationId
, undefined);
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);
81 it('covers unknown password hash', async
function () {
82 authenticator
.db
.authenticationGet
.resolves({
84 credential: '$other$kind_of_credential',
86 const authString
= `${identifier}:wrongPassword}`;
87 const result
= await authenticator
.isValidBasic(authString
, ctx
);
88 assert
.strictEqual(result
, false);
89 assert
.strictEqual(ctx
.authenticationId
, undefined);
93 describe('isValidIdentifierCredential', function () {
94 it('succeeds', async
function () {
95 _authMechanismRequired(authenticator
, 'argon2');
96 authenticator
.db
.authenticationGet
.resolves({
100 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
101 assert
.strictEqual(result
, true);
102 assert
.strictEqual(ctx
.authenticationId
, identifier
);
104 it('fails', async
function () {
105 _authMechanismRequired(authenticator
, 'argon2');
106 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 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);
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);
126 it('covers unknown password hash', async
function () {
127 authenticator
.db
.authenticationGet
.resolves({
129 credential: '$other$kind_of_credential',
131 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
132 assert
.strictEqual(result
, false);
133 assert
.strictEqual(ctx
.authenticationId
, undefined);
135 it('covers PAM', async
function () {
136 _authMechanismRequired(authenticator
, 'pam');
137 sinon
.stub(authenticator
, '_isValidPAMIdentifier').resolves(true);
138 authenticator
.db
.authenticationGet
.resolves({
142 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
143 assert
.strictEqual(result
, true);
144 assert
.strictEqual(ctx
.authenticationId
, identifier
);
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
);
152 }); // isValidIdentifierCredential
154 describe('_isValidPAMIdentifier', function () {
155 beforeEach(function () {
156 _authMechanismRequired(authenticator
, 'pam');
157 sinon
.stub(authenticator
.authn
.pam
, 'pamAuthenticatePromise');
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);
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);
170 it('covers error', async
function () {
171 _authMechanismRequired(authenticator
, 'pam');
172 const expected
= new Error('blah');
173 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(expected
);
175 await authenticator
._isValidPAMIdentifier(identifier
, credential
);
176 assert
.fail(noExpectedException
);
178 assert
.deepStrictEqual(e
, expected
);
181 it('covers forbidden', async
function () {
183 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
184 assert
.strictEqual(result
, false);
186 }); // _isValidPAMIdentifier
188 describe('_cookieParse', function () {
189 it('covers empty', function () {
191 const result
= Authenticator
._cookieParse();
192 assert
.deepStrictEqual(result
, expected
);
194 it('covers non variable', function () {
195 const cookie
= 'foo';
199 const result
= Authenticator
._cookieParse(cookie
);
200 assert
.deepStrictEqual(result
, expected
);
202 it('parses cookie', function () {
203 const cookie
= 'foo=bar; baz="quux"';
208 const result
= Authenticator
._cookieParse(cookie
);
209 assert
.deepStrictEqual(result
, expected
);
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
);
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
);
227 }); // isValidAuthorization
229 describe('requestBasic', function () {
230 it('covers', function () {
235 authenticator
.requestBasic(res
);
236 assert
.fail(noExpectedException
);
238 assert(e
instanceof Errors
.ResponseError
);
239 assert
.strictEqual(e
.statusCode
, Enum
.ErrorResponse
.Unauthorized
.statusCode
);
244 describe('isValidCookieAuth', function () {
246 beforeEach(function () {
247 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
248 cookie
= 'squeepSession=dummy';
250 it('covers identifier success', async
function () {
251 authenticator
.mysteryBox
.unpack
.resolves({
252 authenticatedIdentifier: 'identifier',
254 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
255 assert
.strictEqual(result
, true);
257 it('covers profile success', async
function () {
258 authenticator
.mysteryBox
.unpack
.resolves({
259 authenticatedProfile: 'profile',
261 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
262 assert
.strictEqual(result
, true);
264 it('covers missing cookie', async
function () {
265 cookie
= 'wrong=cookie';
266 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
267 assert
.strictEqual(result
, false);
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);
274 it('covers broken session', async
function () {
275 authenticator
.mysteryBox
.unpack
.resolves({
278 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
279 assert
.strictEqual(result
, false);
281 }); // isValidCookieAuth
283 describe('sessionCheck', function () {
284 let cookie
, req
, res
, loginPath
, required
, profilesAllowed
;
285 beforeEach(function () {
286 cookie
= 'squeepSession=sessionCookie';
287 ctx
.clientProtocol
= 'https';
289 getHeader: sinon
.stub(),
293 setHeader: sinon
.stub(),
295 loginPath
= '/admin/login';
297 profilesAllowed
= true;
299 it('covers valid cookie session', async
function () {
300 req
.getHeader
.returns(cookie
);
301 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
303 authenticatedIdentifier: 'user',
305 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
306 assert
.strictEqual(result
, true);
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);
313 authenticatedIdentifier: 'user',
315 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
316 assert
.strictEqual(result
, true);
318 it('rejects insecure connection', async
function () {
319 ctx
.clientProtocol
= 'http';
321 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
322 assert
.fail(noExpectedException
);
324 assert(e
instanceof Errors
.ResponseError
);
325 assert
.strictEqual(e
.statusCode
, Enum
.ErrorResponse
.Forbidden
.statusCode
);
328 it('ignores insecure connection if auth not required', async
function () {
329 ctx
.clientProtocol
= 'http';
331 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
332 assert
.strictEqual(result
, false);
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
);
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);
345 authenticatedIdentifier: 'user',
347 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
348 assert
.strictEqual(result
, true);
350 it('redirects with profile', async
function () {
351 req
.getHeader
.returns(cookie
);
352 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
354 authenticatedProfile: 'user',
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
);
361 }); // sessionRequiredLocal
362 describe('sessionRequired', function () {
363 it('accepts identifier', async
function () {
364 req
.getHeader
.returns(cookie
);
365 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
367 authenticatedIdentifier: 'user',
369 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
370 assert
.strictEqual(result
, true);
372 it('accepts profile', async
function () {
373 req
.getHeader
.returns(cookie
);
374 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
376 authenticatedProfile: 'user',
378 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
379 assert
.strictEqual(result
, true);
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
);
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
);
398 }); // sessionRequired
399 describe('sessionOptionalLocal', function () {
400 it('rejects profile', async
function () {
401 req
.getHeader
.returns(cookie
);
402 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
404 authenticatedProfile: 'user',
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
);
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
);
421 }); // sessionOptional
422 }); // convenience wrappers
425 describe('apiRequiredLocal', function () {
427 beforeEach(function () {
430 getHeader: sinon
.stub(),
434 setHeader: sinon
.stub(),
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
);
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);
451 await authenticator
.apiRequiredLocal(req
, res
, ctx
);
452 assert
.fail(noExpectedException
);
454 assert
.strictEqual(e
.statusCode
, 401);
455 assert(!authenticator
.sessionCheck
.called
);
456 assert(authenticator
.isValidAuthorization
.called
);
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
);
468 it('covers missing basic auth, ignores session', async
function () {
469 req
.getHeader
.returns();
470 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
472 await authenticator
.apiRequiredLocal(req
, res
, ctx
, false);
473 assert
.fail(noExpectedException
);
475 assert
.strictEqual(e
.statusCode
, 401);
476 assert(!authenticator
.sessionCheck
.called
);
477 assert(!authenticator
.isValidAuthorization
.called
);
478 assert(res
.setHeader
.called
);
481 }); // apiRequiredLocal