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('rejects insecure connection', async
function () {
309 ctx
.clientProtocol
= 'http';
311 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
312 assert
.fail(noExpectedException
);
314 assert(e
instanceof Errors
.ResponseError
);
315 assert
.strictEqual(e
.statusCode
, Enum
.ErrorResponse
.Forbidden
.statusCode
);
318 it('ignores insecure connection if auth not required', async
function () {
319 ctx
.clientProtocol
= 'http';
321 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
322 assert
.strictEqual(result
, false);
324 it('redirects without any auth', async
function () {
325 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
326 assert(res
.end
.called
);
327 assert(res
.setHeader
.called
);
329 describe('convenience wrappers', function () {
330 describe('sessionRequiredLocal', function () {
331 it('accepts identifier', async
function () {
332 req
.getHeader
.returns(cookie
);
333 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
335 authenticatedIdentifier: 'user',
337 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
338 assert
.strictEqual(result
, true);
340 it('redirects with profile', async
function () {
341 req
.getHeader
.returns(cookie
);
342 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
344 authenticatedProfile: 'user',
346 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
347 assert
.strictEqual(result
, false);
348 assert(res
.end
.called
);
349 assert(res
.setHeader
.called
);
351 }); // sessionRequiredLocal
352 describe('sessionRequired', function () {
353 it('accepts identifier', async
function () {
354 req
.getHeader
.returns(cookie
);
355 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
357 authenticatedIdentifier: 'user',
359 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
360 assert
.strictEqual(result
, true);
362 it('accepts profile', async
function () {
363 req
.getHeader
.returns(cookie
);
364 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
366 authenticatedProfile: 'user',
368 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
369 assert
.strictEqual(result
, true);
371 it('rejects invalid', async
function () {
372 req
.getHeader
.returns(cookie
);
373 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
374 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
375 assert
.strictEqual(result
, false);
376 assert(res
.end
.called
);
377 assert(res
.setHeader
.called
);
379 it('covers insecure allowed', async
function () {
380 authenticator
.options
.authenticator
.secureAuthOnly
= false;
381 req
.getHeader
.returns(cookie
);
382 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
383 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
384 assert
.strictEqual(result
, false);
385 assert(res
.end
.called
);
386 assert(res
.setHeader
.called
);
388 }); // sessionRequired
389 describe('sessionOptionalLocal', function () {
390 it('rejects profile', async
function () {
391 req
.getHeader
.returns(cookie
);
392 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
394 authenticatedProfile: 'user',
396 const result
= await authenticator
.sessionOptionalLocal(req
, res
, ctx
, loginPath
);
397 assert
.strictEqual(result
, false);
398 assert(!res
.end
.called
);
399 assert(!res
.setHeader
.called
);
401 }); // sessionOptionalLocal
402 describe('sessionOptional', function () {
403 it('rejects invalid', async
function () {
404 req
.getHeader
.returns(cookie
);
405 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
406 const result
= await authenticator
.sessionOptional(req
, res
, ctx
, loginPath
);
407 assert
.strictEqual(result
, false);
408 assert(!res
.end
.called
);
409 assert(!res
.setHeader
.called
);
411 }); // sessionOptional
412 }); // convenience wrappers
415 describe('apiRequiredLocal', function () {
417 beforeEach(function () {
420 getHeader: sinon
.stub(),
424 setHeader: sinon
.stub(),
427 it('covers valid basic auth', async
function () {
428 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
429 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
430 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
431 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
432 assert
.strictEqual(result
, true);
433 assert(authenticator
.isValidAuthorization
.called
);
434 assert(!authenticator
.sessionCheck
.called
);
436 it('covers invalid basic auth', async
function () {
437 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
438 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
439 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
441 await authenticator
.apiRequiredLocal(req
, res
, ctx
);
442 assert
.fail(noExpectedException
);
444 assert
.strictEqual(e
.statusCode
, 401);
445 assert(!authenticator
.sessionCheck
.called
);
446 assert(authenticator
.isValidAuthorization
.called
);
449 it('covers missing basic auth, valid session', async
function () {
450 req
.getHeader
.returns();
451 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
452 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
453 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
454 assert
.strictEqual(result
, true);
455 assert(!authenticator
.isValidAuthorization
.called
);
456 assert(authenticator
.sessionCheck
.called
);
458 it('covers missing basic auth, ignores session', async
function () {
459 req
.getHeader
.returns();
460 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
462 await authenticator
.apiRequiredLocal(req
, res
, ctx
, false);
463 assert
.fail(noExpectedException
);
465 assert
.strictEqual(e
.statusCode
, 401);
466 assert(!authenticator
.sessionCheck
.called
);
467 assert(!authenticator
.isValidAuthorization
.called
);
468 assert(res
.setHeader
.called
);
471 }); // apiRequiredLocal