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