set SameSite to Lax on session cookies
[squeep-authentication-module] / test / lib / authenticator.js
1 /* eslint-env mocha */
2 'use strict';
3
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');
12
13 const noExpectedException = 'did not receive expected exception';
14
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
19 this.skip();
20 }
21 }
22
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';
28 ctx = {};
29 password = 'badPassword';
30 stubDb._reset();
31 });
32 afterEach(function () {
33 sinon.restore();
34 });
35
36 it('covers no auth mechanisms', function () {
37 options.authenticator.authnEnabled = [];
38 try {
39 authenticator = new Authenticator(stubLogger, stubDb, options);
40 assert.fail(noExpectedException);
41 } catch (e) {
42 assert.strictEqual(e.message, 'no authentication mechanisms available');
43 }
44 });
45
46 it('covers empty realm', function () {
47 options.authenticator.basicRealm = undefined;
48 authenticator = new Authenticator(stubLogger, stubDb, options);
49 });
50
51 describe('isValidBasic', function () {
52 it('succeeds', async function () {
53 _authMechanismRequired(authenticator, 'argon2');
54 authenticator.db.authenticationGet.resolves({
55 identifier,
56 credential,
57 });
58 const authString = `${identifier}:${password}`;
59 const result = await authenticator.isValidBasic(authString, ctx);
60 assert.strictEqual(result, true);
61 assert.strictEqual(ctx.authenticationId, identifier);
62 });
63 it('fails', async function () {
64 _authMechanismRequired(authenticator, 'argon2');
65 authenticator.db.authenticationGet.resolves({
66 identifier,
67 credential,
68 });
69 const authString = `${identifier}:wrongPassword}`;
70 const result = await authenticator.isValidBasic(authString, ctx);
71 assert.strictEqual(result, false);
72 assert.strictEqual(ctx.authenticationId, undefined);
73 });
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);
80 });
81 it('covers unknown password hash', async function () {
82 authenticator.db.authenticationGet.resolves({
83 identifier,
84 credential: '$other$kind_of_credential',
85 });
86 const authString = `${identifier}:wrongPassword}`;
87 const result = await authenticator.isValidBasic(authString, ctx);
88 assert.strictEqual(result, false);
89 assert.strictEqual(ctx.authenticationId, undefined);
90 });
91 }); // isValidBasic
92
93 describe('isValidIdentifierCredential', function () {
94 it('succeeds', async function () {
95 _authMechanismRequired(authenticator, 'argon2');
96 authenticator.db.authenticationGet.resolves({
97 identifier,
98 credential,
99 });
100 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
101 assert.strictEqual(result, true);
102 assert.strictEqual(ctx.authenticationId, identifier);
103 });
104 it('fails', async function () {
105 _authMechanismRequired(authenticator, 'argon2');
106 authenticator.db.authenticationGet.resolves({
107 identifier,
108 credential,
109 });
110 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
111 assert.strictEqual(result, false);
112 assert.strictEqual(ctx.authenticationId, undefined);
113 });
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);
119 });
120 it('covers unknown password hash', async function () {
121 authenticator.db.authenticationGet.resolves({
122 identifier,
123 credential: '$other$kind_of_credential',
124 });
125 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
126 assert.strictEqual(result, false);
127 assert.strictEqual(ctx.authenticationId, undefined);
128 });
129 it('covers PAM', async function () {
130 _authMechanismRequired(authenticator, 'pam');
131 sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
132 authenticator.db.authenticationGet.resolves({
133 identifier,
134 credential: '$PAM$',
135 });
136 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
137 assert.strictEqual(result, true);
138 assert.strictEqual(ctx.authenticationId, identifier);
139 });
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);
145 });
146 }); // isValidIdentifierCredential
147
148 describe('_isValidPAMIdentifier', function () {
149 beforeEach(function () {
150 _authMechanismRequired(authenticator, 'pam');
151 sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
152 });
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);
157 });
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);
163 });
164 it('covers error', async function () {
165 _authMechanismRequired(authenticator, 'pam');
166 const expected = new Error('blah');
167 authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
168 try {
169 await authenticator._isValidPAMIdentifier(identifier, credential);
170 assert.fail(noExpectedException);
171 } catch (e) {
172 assert.deepStrictEqual(e, expected);
173 }
174 });
175 it('covers forbidden', async function () {
176 identifier = 'root';
177 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
178 assert.strictEqual(result, false);
179 });
180 }); // _isValidPAMIdentifier
181
182 describe('_cookieParse', function () {
183 it('covers empty', function () {
184 const expected = {};
185 const result = Authenticator._cookieParse();
186 assert.deepStrictEqual(result, expected);
187 });
188 it('covers non variable', function () {
189 const cookie = 'foo';
190 const expected = {
191 foo: null,
192 };
193 const result = Authenticator._cookieParse(cookie);
194 assert.deepStrictEqual(result, expected);
195 });
196 it('parses cookie', function () {
197 const cookie = 'foo=bar; baz="quux"';
198 const expected = {
199 foo: 'bar',
200 baz: 'quux',
201 };
202 const result = Authenticator._cookieParse(cookie);
203 assert.deepStrictEqual(result, expected);
204 });
205 }); // _cookieParse
206
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);
214 });
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);
220 });
221 }); // isValidAuthorization
222
223 describe('requestBasic', function () {
224 it('covers', function () {
225 try {
226 const res = {
227 setHeader: () => {},
228 };
229 authenticator.requestBasic(res);
230 assert.fail(noExpectedException);
231 } catch (e) {
232 assert(e instanceof Errors.ResponseError);
233 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
234 }
235 });
236 }); // requestBasic
237
238 describe('isValidCookieAuth', function () {
239 let cookie;
240 beforeEach(function () {
241 sinon.stub(authenticator.mysteryBox, 'unpack');
242 cookie = 'squeepSession=dummy';
243 });
244 it('covers identifier success', async function () {
245 authenticator.mysteryBox.unpack.resolves({
246 authenticatedIdentifier: 'identifier',
247 });
248 const result = await authenticator.isValidCookieAuth(ctx, cookie);
249 assert.strictEqual(result, true);
250 });
251 it('covers profile success', async function () {
252 authenticator.mysteryBox.unpack.resolves({
253 authenticatedProfile: 'profile',
254 });
255 const result = await authenticator.isValidCookieAuth(ctx, cookie);
256 assert.strictEqual(result, true);
257 });
258 it('covers missing cookie', async function () {
259 cookie = 'wrong=cookie';
260 const result = await authenticator.isValidCookieAuth(ctx, cookie);
261 assert.strictEqual(result, false);
262 });
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);
267 });
268 it('covers broken session', async function () {
269 authenticator.mysteryBox.unpack.resolves({
270 randomData: 'foo',
271 });
272 const result = await authenticator.isValidCookieAuth(ctx, cookie);
273 assert.strictEqual(result, false);
274 });
275 }); // isValidCookieAuth
276
277 describe('sessionCheck', function () {
278 let cookie, req, res, loginPath, required, profilesAllowed;
279 beforeEach(function () {
280 cookie = 'squeepSession=sessionCookie';
281 ctx.clientProtocol = 'https';
282 req = {
283 getHeader: sinon.stub(),
284 };
285 res = {
286 end: sinon.stub(),
287 setHeader: sinon.stub(),
288 };
289 loginPath = '/admin/login';
290 required = true;
291 profilesAllowed = true;
292 });
293 it('covers valid cookie session', async function () {
294 req.getHeader.returns(cookie);
295 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
296 ctx.session = {
297 authenticatedIdentifier: 'user',
298 };
299 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
300 assert.strictEqual(result, true);
301 });
302 it('rejects insecure connection', async function () {
303 ctx.clientProtocol = 'http';
304 try {
305 await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
306 assert.fail(noExpectedException);
307 } catch (e) {
308 assert(e instanceof Errors.ResponseError);
309 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
310 }
311 });
312 it('ignores insecure connection if auth not required', async function () {
313 ctx.clientProtocol = 'http';
314 required = false;
315 const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
316 assert.strictEqual(result, false);
317 });
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);
322 });
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);
328 ctx.session = {
329 authenticatedIdentifier: 'user',
330 };
331 const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
332 assert.strictEqual(result, true);
333 });
334 it('redirects with profile', async function () {
335 req.getHeader.returns(cookie);
336 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
337 ctx.session = {
338 authenticatedProfile: 'user',
339 };
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);
344 });
345 }); // sessionRequiredLocal
346 describe('sessionRequired', function () {
347 it('accepts identifier', async function () {
348 req.getHeader.returns(cookie);
349 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
350 ctx.session = {
351 authenticatedIdentifier: 'user',
352 };
353 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
354 assert.strictEqual(result, true);
355 });
356 it('accepts profile', async function () {
357 req.getHeader.returns(cookie);
358 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
359 ctx.session = {
360 authenticatedProfile: 'user',
361 };
362 const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
363 assert.strictEqual(result, true);
364 });
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);
372 });
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);
381 });
382 }); // sessionRequired
383 describe('sessionOptionalLocal', function () {
384 it('rejects profile', async function () {
385 req.getHeader.returns(cookie);
386 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
387 ctx.session = {
388 authenticatedProfile: 'user',
389 };
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);
394 });
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);
404 });
405 }); // sessionOptional
406 }); // convenience wrappers
407 }); // sessionCheck
408
409 describe('apiRequiredLocal', function () {
410 let req, res;
411 beforeEach(function () {
412 ctx = {};
413 req = {
414 getHeader: sinon.stub(),
415 };
416 res = {
417 end: sinon.stub(),
418 setHeader: sinon.stub(),
419 };
420 });
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);
429 });
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 try {
435 await authenticator.apiRequiredLocal(req, res, ctx);
436 assert.fail(noExpectedException);
437 } catch (e) {
438 assert.strictEqual(e.statusCode, 401);
439 assert(!authenticator.sessionCheck.called);
440 assert(authenticator.isValidAuthorization.called);
441 }
442 });
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);
451 });
452 it('covers missing basic auth, ignores session', async function () {
453 req.getHeader.returns();
454 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
455 try {
456 await authenticator.apiRequiredLocal(req, res, ctx, false);
457 assert.fail(noExpectedException);
458 } catch (e) {
459 assert.strictEqual(e.statusCode, 401);
460 assert(!authenticator.sessionCheck.called);
461 assert(!authenticator.isValidAuthorization.called);
462 assert(res.setHeader.called);
463 }
464 });
465 }); // apiRequiredLocal
466
467 }); // Authenticator