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 describe('Authenticator', function () {
14 let authenticator
, credential
, ctx
, identifier
, password
, options
;
15 function _authMechanismRequired(a
, m
) {
16 if (!a
.authn
[m
]) { // eslint-disable-line security/detect-object-injection
21 beforeEach(function () {
22 options
= Config('test');
23 authenticator
= new Authenticator(stubLogger
, stubDb
, options
);
24 identifier
= 'username';
25 credential
= '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
27 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 describe('isValidBasic', function () {
47 it('succeeds', async
function () {
48 _authMechanismRequired(authenticator
, 'argon2');
49 authenticator
.db
.authenticationGet
.resolves({
53 const authString
= `${identifier}:${password}`;
54 const result
= await authenticator
.isValidBasic(authString
, ctx
);
55 assert
.strictEqual(result
, true);
56 assert
.strictEqual(ctx
.authenticationId
, identifier
);
58 it('fails', async
function () {
59 _authMechanismRequired(authenticator
, 'argon2');
60 authenticator
.db
.authenticationGet
.resolves({
64 const authString
= `${identifier}:wrongPassword}`;
65 const result
= await authenticator
.isValidBasic(authString
, ctx
);
66 assert
.strictEqual(result
, false);
67 assert
.strictEqual(ctx
.authenticationId
, undefined);
69 it('covers no entry', async
function() {
70 authenticator
.db
.authenticationGet
.resolves();
71 const authString
= `${identifier}:wrongPassword}`;
72 const result
= await authenticator
.isValidBasic(authString
, ctx
);
73 assert
.strictEqual(result
, false);
74 assert
.strictEqual(ctx
.authenticationId
, undefined);
76 it('covers unknown password hash', async
function () {
77 authenticator
.db
.authenticationGet
.resolves({
79 credential: '$other$kind_of_credential',
81 const authString
= `${identifier}:wrongPassword}`;
82 const result
= await authenticator
.isValidBasic(authString
, ctx
);
83 assert
.strictEqual(result
, false);
84 assert
.strictEqual(ctx
.authenticationId
, undefined);
88 describe('isValidIdentifierCredential', function () {
89 it('succeeds', async
function () {
90 _authMechanismRequired(authenticator
, 'argon2');
91 authenticator
.db
.authenticationGet
.resolves({
95 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
96 assert
.strictEqual(result
, true);
97 assert
.strictEqual(ctx
.authenticationId
, identifier
);
99 it('fails', async
function () {
100 _authMechanismRequired(authenticator
, 'argon2');
101 authenticator
.db
.authenticationGet
.resolves({
105 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
106 assert
.strictEqual(result
, false);
107 assert
.strictEqual(ctx
.authenticationId
, undefined);
109 it('covers no entry', async
function() {
110 authenticator
.db
.authenticationGet
.resolves();
111 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
112 assert
.strictEqual(result
, false);
113 assert
.strictEqual(ctx
.authenticationId
, undefined);
115 it('covers non-string credential', async
function () {
116 credential
= '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
117 const result
= await authenticator
.isValidIdentifierCredential(identifier
, undefined, ctx
);
118 assert
.strictEqual(result
, false);
119 assert
.strictEqual(ctx
.authenticationId
, undefined);
121 it('covers unknown password hash', async
function () {
122 authenticator
.db
.authenticationGet
.resolves({
124 credential: '$other$kind_of_credential',
126 const result
= await authenticator
.isValidIdentifierCredential(identifier
, 'wrongPassword', ctx
);
127 assert
.strictEqual(result
, false);
128 assert
.strictEqual(ctx
.authenticationId
, undefined);
130 it('covers PAM', async
function () {
131 _authMechanismRequired(authenticator
, 'pam');
132 sinon
.stub(authenticator
, '_isValidPAMIdentifier').resolves(true);
133 authenticator
.db
.authenticationGet
.resolves({
137 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
138 assert
.strictEqual(result
, true);
139 assert
.strictEqual(ctx
.authenticationId
, identifier
);
141 it('covers debug', async
function () {
142 authenticator
.authnEnabled
= ['DEBUG_ANY'];
143 const result
= await authenticator
.isValidIdentifierCredential(identifier
, password
, ctx
);
144 assert
.strictEqual(result
, true);
145 assert
.strictEqual(ctx
.authenticationId
, identifier
);
147 }); // isValidIdentifierCredential
149 describe('_isValidPAMIdentifier', function () {
150 beforeEach(function () {
151 _authMechanismRequired(authenticator
, 'pam');
152 sinon
.stub(authenticator
.authn
.pam
, 'pamAuthenticatePromise');
154 it('covers success', async
function () {
155 authenticator
.authn
.pam
.pamAuthenticatePromise
.resolves(true);
156 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
157 assert
.strictEqual(result
, true);
159 it('covers failure', async
function () {
160 _authMechanismRequired(authenticator
, 'pam');
161 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(new authenticator
.authn
.pam
.PamError());
162 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
163 assert
.strictEqual(result
, false);
165 it('covers error', async
function () {
166 _authMechanismRequired(authenticator
, 'pam');
167 const expected
= new Error('blah');
168 authenticator
.authn
.pam
.pamAuthenticatePromise
.rejects(expected
);
169 assert
.rejects(() => authenticator
._isValidPAMIdentifier(identifier
, credential
), expected
);
171 it('covers forbidden', async
function () {
173 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
174 assert
.strictEqual(result
, false);
176 }); // _isValidPAMIdentifier
178 describe('_cookieParse', function () {
179 it('covers empty', function () {
181 const result
= Authenticator
._cookieParse();
182 assert
.deepStrictEqual(result
, expected
);
184 it('covers non variable', function () {
185 const cookie
= 'foo';
189 const result
= Authenticator
._cookieParse(cookie
);
190 assert
.deepStrictEqual(result
, expected
);
192 it('parses cookie', function () {
193 const cookie
= 'foo=bar; baz="quux"';
198 const result
= Authenticator
._cookieParse(cookie
);
199 assert
.deepStrictEqual(result
, expected
);
203 describe('isValidAuthorization', function () {
204 it('handles basic', async
function () {
205 const expected
= true;
206 const authorizationHeader
= 'basic Zm9vOmJhcg==';
207 sinon
.stub(authenticator
, 'isValidBasic').resolves(expected
);
208 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
209 assert
.strictEqual(result
, expected
);
211 it('handles other', async
function () {
212 const expected
= false;
213 const authorizationHeader
= 'bearer Zm9vOmJhcg==';
214 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
215 assert
.strictEqual(result
, expected
);
217 }); // isValidAuthorization
219 describe('requestBasic', function () {
220 it('covers', function () {
224 assert
.throws(() => authenticator
.requestBasic(res
), {
225 name: 'ResponseError',
226 statusCode: Enum
.ErrorResponse
.Unauthorized
.statusCode
,
231 describe('isValidCookieAuth', function () {
233 beforeEach(function () {
234 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
235 cookie
= 'squeepSession=dummy';
237 it('covers identifier success', async
function () {
238 authenticator
.mysteryBox
.unpack
.resolves({
239 authenticatedIdentifier: 'identifier',
241 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
242 assert
.strictEqual(result
, true);
244 it('covers profile success', async
function () {
245 authenticator
.mysteryBox
.unpack
.resolves({
246 authenticatedProfile: 'profile',
248 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
249 assert
.strictEqual(result
, true);
251 it('covers missing cookie', async
function () {
252 cookie
= 'wrong=cookie';
253 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
254 assert
.strictEqual(result
, false);
256 it('covers bad cookie', async
function () {
257 authenticator
.mysteryBox
.unpack
.rejects();
258 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
259 assert
.strictEqual(result
, false);
261 it('covers broken session', async
function () {
262 authenticator
.mysteryBox
.unpack
.resolves({
265 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
266 assert
.strictEqual(result
, false);
268 }); // isValidCookieAuth
270 describe('sessionCheck', function () {
271 let cookie
, req
, res
, loginPath
, required
, profilesAllowed
;
272 beforeEach(function () {
273 cookie
= 'squeepSession=sessionCookie';
274 ctx
.clientProtocol
= 'https';
276 getHeader: sinon
.stub(),
280 setHeader: sinon
.stub(),
282 loginPath
= '/admin/login';
284 profilesAllowed
= true;
286 it('covers valid cookie session', async
function () {
287 req
.getHeader
.returns(cookie
);
288 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
290 authenticatedIdentifier: 'user',
292 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
293 assert
.strictEqual(result
, true);
295 it('covers valid insecure cookie session', async
function () {
296 authenticator
.secureAuthOnly
= false;
297 req
.getHeader
.returns(cookie
);
298 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
300 authenticatedIdentifier: 'user',
302 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
303 assert
.strictEqual(result
, true);
305 it('rejects insecure connection', async
function () {
306 ctx
.clientProtocol
= 'http';
307 assert
.rejects(() => authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
), {
308 name: 'ResponseError',
309 sttausCode: Enum
.ErrorResponse
.Forbidden
.statusCode
,
312 it('ignores insecure connection if auth not required', async
function () {
313 ctx
.clientProtocol
= 'http';
315 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
316 assert
.strictEqual(result
, false);
318 it('redirects without any auth', async
function () {
319 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
320 assert(res
.end
.called
);
321 assert(res
.setHeader
.called
);
323 describe('convenience wrappers', function () {
324 describe('sessionRequiredLocal', function () {
325 it('accepts identifier', async
function () {
326 req
.getHeader
.returns(cookie
);
327 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
329 authenticatedIdentifier: 'user',
331 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
332 assert
.strictEqual(result
, true);
334 it('redirects with profile', async
function () {
335 req
.getHeader
.returns(cookie
);
336 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
338 authenticatedProfile: 'user',
340 const result
= await authenticator
.sessionRequiredLocal(req
, res
, ctx
, loginPath
);
341 assert
.strictEqual(result
, false);
342 assert(res
.end
.called
);
343 assert(res
.setHeader
.called
);
345 }); // sessionRequiredLocal
346 describe('sessionRequired', function () {
347 it('accepts identifier', async
function () {
348 req
.getHeader
.returns(cookie
);
349 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
351 authenticatedIdentifier: 'user',
353 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
354 assert
.strictEqual(result
, true);
356 it('accepts profile', async
function () {
357 req
.getHeader
.returns(cookie
);
358 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
360 authenticatedProfile: 'user',
362 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
363 assert
.strictEqual(result
, true);
365 it('rejects invalid', async
function () {
366 req
.getHeader
.returns(cookie
);
367 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
368 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
369 assert
.strictEqual(result
, false);
370 assert(res
.end
.called
);
371 assert(res
.setHeader
.called
);
373 it('covers insecure allowed', async
function () {
374 authenticator
.options
.authenticator
.secureAuthOnly
= false;
375 req
.getHeader
.returns(cookie
);
376 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
377 const result
= await authenticator
.sessionRequired(req
, res
, ctx
, loginPath
);
378 assert
.strictEqual(result
, false);
379 assert(res
.end
.called
);
380 assert(res
.setHeader
.called
);
382 }); // sessionRequired
383 describe('sessionOptionalLocal', function () {
384 it('rejects profile', async
function () {
385 req
.getHeader
.returns(cookie
);
386 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
388 authenticatedProfile: 'user',
390 const result
= await authenticator
.sessionOptionalLocal(req
, res
, ctx
, loginPath
);
391 assert
.strictEqual(result
, false);
392 assert(!res
.end
.called
);
393 assert(!res
.setHeader
.called
);
395 }); // sessionOptionalLocal
396 describe('sessionOptional', function () {
397 it('rejects invalid', async
function () {
398 req
.getHeader
.returns(cookie
);
399 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(false);
400 const result
= await authenticator
.sessionOptional(req
, res
, ctx
, loginPath
);
401 assert
.strictEqual(result
, false);
402 assert(!res
.end
.called
);
403 assert(!res
.setHeader
.called
);
405 }); // sessionOptional
406 }); // convenience wrappers
409 describe('apiRequiredLocal', function () {
411 beforeEach(function () {
414 getHeader: sinon
.stub(),
418 setHeader: sinon
.stub(),
421 it('covers valid basic auth', async
function () {
422 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
423 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
424 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
425 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
426 assert
.strictEqual(result
, true);
427 assert(authenticator
.isValidAuthorization
.called
);
428 assert(!authenticator
.sessionCheck
.called
);
430 it('covers invalid basic auth', async
function () {
431 req
.getHeader
.returns('Basic Zm9vOmJhcg==');
432 sinon
.stub(authenticator
, 'sessionCheck').resolves(false);
433 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
434 assert
.rejects(() => authenticator
.apiRequiredLocal(req
, res
, ctx
), {
435 name: 'ResponseError',
438 assert(!authenticator
.sessionCheck
.called
);
439 assert(authenticator
.isValidAuthorization
.called
);
441 it('covers missing basic auth, valid session', async
function () {
442 req
.getHeader
.returns();
443 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
444 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
445 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
446 assert
.strictEqual(result
, true);
447 assert(!authenticator
.isValidAuthorization
.called
);
448 assert(authenticator
.sessionCheck
.called
);
450 it('covers missing basic auth, ignores session', async
function () {
451 req
.getHeader
.returns();
452 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
453 assert
.rejects(authenticator
.apiRequiredLocal(req
, res
, ctx
, false), {
454 name: 'ResponseError',
457 assert(!authenticator
.sessionCheck
.called
);
458 assert(!authenticator
.isValidAuthorization
.called
);
459 assert(res
.setHeader
.called
);
461 }); // apiRequiredLocal