auth cleanups
[urlittler] / test / src / authenticator.js
1 'use strict';
2
3 const { StubLogger } = require('@squeep/test-helper');
4 const assert = require('node:assert');
5 const sinon = require('sinon');
6
7 const Authenticator = require('../../src/authenticator');
8 const { Errors: { ResponseError } } = require('@squeep/api-dingus');
9
10
11 const noExpectedException = 'did not get expected exception';
12
13 describe('Authenticator', function () {
14 let authenticator, logger, db, options;
15
16 beforeEach(function () {
17 logger = new StubLogger(sinon);
18 db = {
19 context: async (fn) => fn({}),
20 getAuthById: async () => {},
21 getLinkById: async () => {},
22 upsertAuth: async () => {},
23 };
24 authenticator = new Authenticator(logger, db, options);
25 });
26
27 afterEach(function () {
28 sinon.restore();
29 });
30
31 describe('b64 armor', function () {
32 it('reciprocates', function () {
33 const src = '3qmOlsGY5I3qY4O/tqF7LuvS400BBPh/AMzbyoIlvyXPwMA1tg==';
34
35 const armored = Authenticator.b64Armor(src);
36 const unarmored = Authenticator.b64Unarmor(armored);
37
38 assert.strictEqual(unarmored, src);
39 });
40 }); // b64 armor
41
42 describe('generateToken', function () {
43 it('generates a token', async function () {
44 const result = await Authenticator.generateToken();
45 assert.strictEqual(result.length, 36 * 4 / 3);
46 });
47 }); // generateToken
48
49 describe('signature', function () {
50 it('generates a signature', async function () {
51 const secret = 'secret';
52 const id = 'identifier';
53 const epoch = '1602887798';
54 const body = 'some data';
55 const expected = 'P5IXYXu5a7aobQvRinPQlN_k1g8GHycRpx3JrK1O7YJlqmhv3WRP5M3ubObPdUWM';
56 const result = Authenticator.signature(secret, id, epoch, body);
57 assert.strictEqual(result, expected);
58 });
59 }); // generateToken
60
61 describe('requestSignature', function () {
62 let res;
63
64 beforeEach(function () {
65 res = {
66 setHeader: sinon.stub(),
67 };
68 });
69
70 it('requests custom auth', function () {
71 try {
72 authenticator.requestSignature(res);
73 assert.fail(noExpectedException);
74 } catch (e) {
75 assert(e instanceof ResponseError);
76 assert.strictEqual(e.statusCode, 401);
77 assert(res.setHeader.called);
78 }
79 });
80 }); // requestSignature
81
82 describe('isValidSignature', function () {
83 let reqSig, ctx;
84
85 beforeEach(function () {
86 reqSig = undefined;
87 ctx = {
88 rawBody: 'all about werewolves',
89 };
90 sinon.stub(authenticator.db, 'getAuthById').resolves({ secret: 'bluemoon' });
91 });
92
93 it('passes valid signature', async function () {
94 reqSig = 'awoo:1604155860:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
95 sinon.stub(Date, 'now').returns(1604155861 * 1000);
96 const result = await authenticator.isValidSignature(reqSig, ctx);
97 assert.strictEqual(result, true);
98 assert.strictEqual(ctx.authenticationId, 'awoo');
99 });
100
101 it('fails invalid data', async function () {
102 reqSig = 'not a signature';
103 sinon.stub(Date, 'now').returns(1604155861 * 1000);
104 const result = await authenticator.isValidSignature(reqSig, ctx);
105 assert.strictEqual(result, false);
106 });
107
108 it('fails missing data', async function () {
109 sinon.stub(Date, 'now').returns(1604155861 * 1000);
110 const result = await authenticator.isValidSignature(reqSig, ctx);
111 assert.strictEqual(result, false);
112 });
113
114 it('fails invalid signature', async function () {
115 reqSig = 'awoo:1604155860:bad signature';
116 sinon.stub(Date, 'now').returns(1604155861 * 1000);
117 const result = await authenticator.isValidSignature(reqSig, ctx);
118 assert.strictEqual(result, false);
119 });
120
121 it('fails invalid timestamp', async function () {
122 reqSig = 'awoo:0:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
123 sinon.stub(Date, 'now').returns(1604155861 * 1000);
124 const result = await authenticator.isValidSignature(reqSig, ctx);
125 assert.strictEqual(result, false);
126 });
127
128 it('fails invalid auth id', async function () {
129 reqSig = 'awoo:1604155860:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
130 authenticator.db.getAuthById.restore();
131 sinon.stub(authenticator.db, 'getAuthById').resolves({});
132 sinon.stub(Date, 'now').returns(1604155861 * 1000);
133 const result = await authenticator.isValidSignature(reqSig, ctx);
134 assert.strictEqual(result, false);
135 });
136 }); // isValidSignature
137
138 describe('isValidToken', function () {
139 let token, linkId;
140
141 beforeEach(function () {
142 token = undefined;
143 linkId = 'identifier';
144 sinon.stub(authenticator.db, 'getLinkById');
145 });
146
147 it('accepts token', async function () {
148 token = 'this_is_a_token';
149 authenticator.db.getLinkById.resolves({ authToken: token });
150 const result = await authenticator.isValidToken(linkId, token);
151 assert.strictEqual(result, true);
152 });
153
154 it('rejects missing token', async function () {
155 const result = await authenticator.isValidToken(linkId, token);
156 assert.strictEqual(result, false);
157 });
158
159 it('rejects wrong token', async function () {
160 token = 'this_is_a_token';
161 authenticator.db.getLinkById.resolves({ authToken: 'some_other_token' });
162 const result = await authenticator.isValidToken(linkId, token);
163 assert.strictEqual(result, false);
164 });
165
166 it('rejects missing id', async function () {
167 token = 'this_is_a_token';
168 linkId = undefined;
169 authenticator.db.getLinkById.resolves();
170 const result = await authenticator.isValidToken(linkId, token);
171 assert.strictEqual(result, false);
172 });
173
174 it('rejects invalid id', async function () {
175 token = 'this_is_a_token';
176 const result = await authenticator.isValidToken(linkId, token);
177 assert.strictEqual(result, false);
178 });
179 }); // isValidToken
180
181 describe('isValidBasic', function () {
182 let credentials, ctx;
183
184 beforeEach(function () {
185 sinon.stub(authenticator, 'requestBasic');
186 sinon.stub(authenticator.db, 'getAuthById');
187 sinon.stub(authenticator.db, 'upsertAuth');
188 credentials = 'id:password';
189 ctx = {};
190 });
191
192 it('accepts plain credential and migrates to hash', async function () {
193 authenticator.db.getAuthById.resolves({ password: 'password' });
194 const result = await authenticator.isValidBasic(credentials, ctx);
195 assert.strictEqual(result, true);
196 assert.strictEqual(ctx.authenticationId, 'id');
197 assert(authenticator.db.upsertAuth.called);
198 });
199
200 it('rejects wrong plain credential', async function () {
201 authenticator.db.getAuthById.resolves({ password: 'wrong_password' });
202 const result = await authenticator.isValidBasic(credentials, ctx);
203 assert.strictEqual(result, false);
204 assert(!('authenticationId' in ctx));
205 assert(authenticator.db.upsertAuth.notCalled);
206 });
207
208 it('accepts argon2 credential', async function () {
209 authenticator.db.getAuthById.resolves({ password: '$argon2id$v=19$m=65536,t=3,p=4$AQKIWU5puGDs3zKIPMo0Ew$Mzl/kzJE6/oRtJLHoGXaoUtlAiXs5HK2qLgHWF6euF8' });
210 const result = await authenticator.isValidBasic(credentials, ctx);
211 assert.strictEqual(result, true);
212 assert.strictEqual(ctx.authenticationId, 'id');
213 assert(authenticator.db.upsertAuth.notCalled);
214 });
215
216 it('rejects wrong argon2 credential', async function () {
217 authenticator.db.getAuthById.resolves({ password: '$argon2id$v=19$m=65536,t=3,p=4$BCPlf0NBgjyXOxdyUDs/FQ$wV4jERm50yByCpSr8lrD8Nu0uVPUsQcVghJQoix5ido' });
218 const result = await authenticator.isValidBasic(credentials, ctx);
219 assert.strictEqual(result, false);
220 assert(!('authenticationId' in ctx));
221 assert(authenticator.db.upsertAuth.notCalled);
222 });
223
224 it('rejects missing id', async function () {
225 authenticator.db.getAuthById.resolves();
226 const result = await authenticator.isValidBasic(credentials, ctx);
227 assert.strictEqual(result, false);
228 assert(!('authenticationId' in ctx));
229 assert(authenticator.db.upsertAuth.notCalled);
230 });
231 }); // isValidBasic
232
233 describe('isValidBearer', function () {
234 let credentials, ctx;
235
236 beforeEach(function () {
237 credentials = 'id:password';
238 ctx = {
239 params: {},
240 };
241 });
242
243 it('accepts token', async function () {
244 ctx.params.id = 'identifier';
245 credentials = 'token';
246 sinon.stub(authenticator.db, 'getLinkById').resolves({ authToken: 'token' });
247 const result = await authenticator.isValidBearer(credentials, ctx);
248 assert.strictEqual(result, true);
249 });
250
251 it('rejects wrong token', async function () {
252 ctx.params.id = 'identifier';
253 sinon.stub(authenticator.db, 'getLinkById').resolves({ authToken: 'wrong_token' });
254 const result = await authenticator.isValidBearer(credentials, ctx);
255 assert.strictEqual(result, false);
256 });
257
258 it('rejects missing id', async function () {
259 sinon.stub(authenticator.db, 'getLinkById').resolves();
260 const result = await authenticator.isValidBearer(credentials, ctx);
261 assert.strictEqual(result, false);
262 });
263 }); // isValidBearer
264
265 describe('isValidAuthorization', function () {
266 let header, ctx;
267
268 beforeEach(function () {
269 header = undefined;
270 sinon.stub(authenticator, 'isValidBasic');
271 sinon.stub(authenticator, 'isValidBearer');
272 sinon.stub(authenticator, 'requestBasic');
273 });
274
275 it('dispatches basic', async function () {
276 header = 'Basic blahblahblah=';
277 await authenticator.isValidAuthorization(header, ctx);
278 assert(authenticator.isValidBasic.called);
279 });
280
281 it('dispatches bearer', async function () {
282 header = 'Bearer blahblahblah=';
283 await authenticator.isValidAuthorization(header, ctx);
284 assert(authenticator.isValidBearer.called);
285 });
286
287 it('handles fallback', async function () {
288 header = 'Digest blahblahblah=';
289 const result = await authenticator.isValidAuthorization(header, ctx);
290 assert.strictEqual(result, false);
291 });
292 }); // isValidAuthorization
293
294 describe('requestBasic', function () {
295 let res;
296
297 beforeEach(function () {
298 res = {
299 setHeader: sinon.stub(),
300 };
301 });
302
303 it('requests custom auth', function () {
304 try {
305 authenticator.requestBasic(res);
306 assert.fail(noExpectedException);
307 } catch (e) {
308 assert(e instanceof ResponseError);
309 assert.strictEqual(e.statusCode, 401);
310 assert(res.setHeader.called);
311 }
312 });
313 }); // requestBasic
314
315 describe('required', function () {
316 let req, res, ctx;
317
318 beforeEach(function () {
319 req = {
320 getHeader: sinon.stub(),
321 };
322 res = {};
323 ctx = {};
324 sinon.stub(authenticator, 'isValidToken').resolves(false);
325 sinon.stub(authenticator, 'isValidSignature').resolves(false);
326 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
327 sinon.stub(authenticator, 'requestBasic');
328 sinon.stub(authenticator, 'requestSignature');
329 });
330
331 it('validates signature auth', async function () {
332 req.getHeader.returns('signature');
333 authenticator.isValidSignature.restore();
334 sinon.stub(authenticator, 'isValidSignature').resolves(true);
335 const result = await authenticator.required(req, res, ctx);
336 assert.strictEqual(result, true);
337 assert(!authenticator.requestBasic.called);
338 });
339
340 it('requests signature auth on signature failure', async function () {
341 req.getHeader.returns('signature');
342 await authenticator.required(req, res, ctx);
343 assert(authenticator.requestSignature.called);
344 });
345
346 it('validates authorization auth', async function () {
347 req.getHeader.onCall(1).returns('signature');
348 authenticator.isValidAuthorization.restore();
349 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
350 const result = await authenticator.required(req, res, ctx);
351 assert.strictEqual(result, true);
352 assert(!authenticator.requestBasic.called);
353 });
354
355 it('requests authorization on auth failure', async function () {
356 req.getHeader.onCall(1).returns('signature');
357 await authenticator.required(req, res, ctx);
358 assert(authenticator.requestBasic.called);
359 });
360
361 it('validates a queryparams token', async function () {
362 ctx.params = { id: 'identifier' };
363 ctx.queryParams = { token: 'token' };
364 authenticator.isValidToken.restore();
365 sinon.stub(authenticator, 'isValidToken').resolves(true);
366 const result = await authenticator.required(req, res, ctx);
367 assert.strictEqual(result, true);
368 assert(!authenticator.requestBasic.called);
369 });
370
371 it('validates a body token', async function () {
372 ctx.params = { id: 'identifier' };
373 ctx.parsedBody = { token: 'token' };
374 authenticator.isValidToken.restore();
375 sinon.stub(authenticator, 'isValidToken').resolves(true);
376 const result = await authenticator.required(req, res, ctx);
377 assert.strictEqual(result, true);
378 assert(!authenticator.requestBasic.called);
379 });
380
381 it('fails invalid token', async function () {
382 ctx.params = { id: 'identifier' };
383 ctx.parsedBody = { token: 'token' };
384 await authenticator.required(req, res, ctx);
385 assert(authenticator.requestBasic.called);
386 });
387
388 it('fails missing token', async function () {
389 ctx.params = { id: 'identifier' };
390 await authenticator.required(req, res, ctx);
391 assert(authenticator.requestBasic.called);
392 });
393
394 it('requests basic when all else fails', async function () {
395 await authenticator.required(req, res, ctx);
396 assert(authenticator.requestBasic.called);
397 });
398
399 }); // required
400
401 describe('optional', function () {
402 let req, res, ctx;
403
404 beforeEach(function () {
405 req = {
406 getHeader: sinon.stub(),
407 };
408 res = {};
409 ctx = {};
410 sinon.stub(authenticator, 'isValidToken').resolves(false);
411 sinon.stub(authenticator, 'isValidSignature').resolves(false);
412 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
413 });
414
415 it('rejects with no auth', async function () {
416 const result = await authenticator.optional(req, res, ctx);
417 assert.strictEqual(result, false);
418 });
419
420 it('validates signature auth', async function () {
421 req.getHeader.onCall(0).returns('signature');
422 authenticator.isValidSignature.restore();
423 sinon.stub(authenticator, 'isValidSignature').resolves(true);
424 const result = await authenticator.optional(req, res, ctx);
425 assert.strictEqual(result, true);
426 });
427
428 it('rejects invalid signature auth', async function () {
429 req.getHeader.onCall(0).returns('signature');
430 const result = await authenticator.optional(req, res, ctx);
431 assert.strictEqual(result, false);
432 });
433
434 it('validates auth', async function () {
435 req.getHeader.onCall(1).returns('basic auth');
436 authenticator.isValidAuthorization.restore();
437 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
438 const result = await authenticator.optional(req, res, ctx);
439 assert.strictEqual(result, true);
440 });
441
442 it('rejects invalid auth', async function () {
443 req.getHeader.onCall(1).returns('basic auth');
444 const result = await authenticator.optional(req, res, ctx);
445 assert.strictEqual(result, false);
446 });
447
448 it('validates queryparam token', async function () {
449 ctx.queryParams = { token: 'token' };
450 ctx.params = { id: 'identifier' };
451 authenticator.isValidToken.restore();
452 sinon.stub(authenticator, 'isValidToken').resolves(true);
453 const result = await authenticator.optional(req, res, ctx);
454 assert.strictEqual(result, true);
455 });
456
457 it('validates body token', async function () {
458 ctx.parsedBody = { token: 'token' };
459 ctx.params = { id: 'identifier' };
460 authenticator.isValidToken.restore();
461 sinon.stub(authenticator, 'isValidToken').resolves(true);
462 const result = await authenticator.optional(req, res, ctx);
463 assert.strictEqual(result, true);
464 });
465
466 it('rejects invalid token', async function () {
467 ctx.queryParams = { token: 'token' };
468 ctx.params = { id: 'identifier' };
469 const result = await authenticator.optional(req, res, ctx);
470 assert.strictEqual(result, false);
471 });
472
473 it('rejects missing token', async function () {
474 ctx.params = { id: 'identifier' };
475 const result = await authenticator.optional(req, res, ctx);
476 assert.strictEqual(result, false);
477 });
478
479 }); // optional
480
481 describe('signResponse', function () {
482 let req, res, ctx;
483 beforeEach(function () {
484 req = {};
485 res = {
486 setHeader: sinon.stub(),
487 };
488 ctx = {};
489 });
490 it('does nothing without auth', function () {
491 authenticator.signResponse(req, res, ctx);
492 assert(!res.setHeader.called);
493 });
494 it('signs a response', function () {
495 ctx.authenticationId = 'identified';
496 ctx.authenticationSecret = 'secret';
497 ctx.responseBody = 'awoo';
498 authenticator.signResponse(req, res, ctx);
499 assert(res.setHeader.called);
500 });
501 it('signs an empty response', function () {
502 ctx.authenticationId = 'identified';
503 ctx.authenticationSecret = 'secret';
504 authenticator.signResponse(req, res, ctx);
505 assert(res.setHeader.called);
506 });
507 it('covers big response non-logging', function () {
508 ctx.responseBody = 'orgle'.repeat(1000);
509 authenticator.signResponse(req, res, ctx);
510 });
511 it('covers buffer response', function () {
512 ctx.responseBody = Buffer.from('orgle'.repeat(1000));
513 authenticator.signResponse(req, res, ctx);
514 });
515 }); // signResponse
516
517 }); // Authenticator