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 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
);
169 await authenticator
._isValidPAMIdentifier(identifier
, credential
);
170 assert
.fail(noExpectedException
);
172 assert
.deepStrictEqual(e
, expected
);
175 it('covers forbidden', async
function () {
177 const result
= await authenticator
._isValidPAMIdentifier(identifier
, credential
);
178 assert
.strictEqual(result
, false);
180 }); // _isValidPAMIdentifier
182 describe('_cookieParse', function () {
183 it('covers empty', function () {
185 const result
= Authenticator
._cookieParse();
186 assert
.deepStrictEqual(result
, expected
);
188 it('covers non variable', function () {
189 const cookie
= 'foo';
193 const result
= Authenticator
._cookieParse(cookie
);
194 assert
.deepStrictEqual(result
, expected
);
196 it('parses cookie', function () {
197 const cookie
= 'foo=bar; baz="quux"';
202 const result
= Authenticator
._cookieParse(cookie
);
203 assert
.deepStrictEqual(result
, expected
);
207 describe('isValidAuthorization', function () {
208 it('handles basic', async
function () {
209 const expected
= true;
210 const authorizationHeader
= 'basic Zm9vOmJhcg==';
211 sinon
.stub(authenticator
, 'isValidBasic').resolves(expected
);
212 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
213 assert
.strictEqual(result
, expected
);
215 it('handles other', async
function () {
216 const expected
= false;
217 const authorizationHeader
= 'bearer Zm9vOmJhcg==';
218 const result
= await authenticator
.isValidAuthorization(authorizationHeader
, ctx
);
219 assert
.strictEqual(result
, expected
);
221 }); // isValidAuthorization
223 describe('requestBasic', function () {
224 it('covers', function () {
229 authenticator
.requestBasic(res
);
230 assert
.fail(noExpectedException
);
232 assert(e
instanceof Errors
.ResponseError
);
233 assert
.strictEqual(e
.statusCode
, Enum
.ErrorResponse
.Unauthorized
.statusCode
);
238 describe('isValidCookieAuth', function () {
240 beforeEach(function () {
241 sinon
.stub(authenticator
.mysteryBox
, 'unpack');
242 cookie
= 'squeepSession=dummy';
244 it('covers identifier success', async
function () {
245 authenticator
.mysteryBox
.unpack
.resolves({
246 authenticatedIdentifier: 'identifier',
248 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
249 assert
.strictEqual(result
, true);
251 it('covers profile success', async
function () {
252 authenticator
.mysteryBox
.unpack
.resolves({
253 authenticatedProfile: 'profile',
255 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
256 assert
.strictEqual(result
, true);
258 it('covers missing cookie', async
function () {
259 cookie
= 'wrong=cookie';
260 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
261 assert
.strictEqual(result
, false);
263 it('covers bad cookie', async
function () {
264 authenticator
.mysteryBox
.unpack
.rejects();
265 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
266 assert
.strictEqual(result
, false);
268 it('covers broken session', async
function () {
269 authenticator
.mysteryBox
.unpack
.resolves({
272 const result
= await authenticator
.isValidCookieAuth(ctx
, cookie
);
273 assert
.strictEqual(result
, false);
275 }); // isValidCookieAuth
277 describe('sessionCheck', function () {
278 let cookie
, req
, res
, loginPath
, required
, profilesAllowed
;
279 beforeEach(function () {
280 cookie
= 'squeepSession=sessionCookie';
281 ctx
.clientProtocol
= 'https';
283 getHeader: sinon
.stub(),
287 setHeader: sinon
.stub(),
289 loginPath
= '/admin/login';
291 profilesAllowed
= true;
293 it('covers valid cookie session', async
function () {
294 req
.getHeader
.returns(cookie
);
295 sinon
.stub(authenticator
, 'isValidCookieAuth').resolves(true);
297 authenticatedIdentifier: 'user',
299 const result
= await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
300 assert
.strictEqual(result
, true);
302 it('rejects insecure connection', async
function () {
303 ctx
.clientProtocol
= 'http';
305 await authenticator
.sessionCheck(req
, res
, ctx
, loginPath
, required
, profilesAllowed
);
306 assert
.fail(noExpectedException
);
308 assert(e
instanceof Errors
.ResponseError
);
309 assert
.strictEqual(e
.statusCode
, 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);
435 await authenticator
.apiRequiredLocal(req
, res
, ctx
);
436 assert
.fail(noExpectedException
);
438 assert
.strictEqual(e
.statusCode
, 401);
439 assert(!authenticator
.sessionCheck
.called
);
440 assert(authenticator
.isValidAuthorization
.called
);
443 it('covers missing basic auth, valid session', async
function () {
444 req
.getHeader
.returns();
445 sinon
.stub(authenticator
, 'sessionCheck').resolves(true);
446 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
447 const result
= await authenticator
.apiRequiredLocal(req
, res
, ctx
);
448 assert
.strictEqual(result
, true);
449 assert(!authenticator
.isValidAuthorization
.called
);
450 assert(authenticator
.sessionCheck
.called
);
452 it('covers missing basic auth, ignores session', async
function () {
453 req
.getHeader
.returns();
454 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(true);
456 await authenticator
.apiRequiredLocal(req
, res
, ctx
, false);
457 assert
.fail(noExpectedException
);
459 assert
.strictEqual(e
.statusCode
, 401);
460 assert(!authenticator
.sessionCheck
.called
);
461 assert(!authenticator
.isValidAuthorization
.called
);
462 assert(res
.setHeader
.called
);
465 }); // apiRequiredLocal