3 const { StubLogger
} = require('@squeep/test-helper');
4 const assert
= require('node:assert');
5 const sinon
= require('sinon');
7 const Authenticator
= require('../../src/authenticator');
8 const { Errors: { ResponseError
} } = require('@squeep/api-dingus');
11 const noExpectedException
= 'did not get expected exception';
13 describe('Authenticator', function () {
14 let authenticator
, logger
, db
, options
;
16 beforeEach(function () {
17 logger
= new StubLogger(sinon
);
19 context: async (fn
) => fn({}),
20 getAuthById: async () => {},
21 getLinkById: async () => {},
22 upsertAuth: async () => {},
24 authenticator
= new Authenticator(logger
, db
, options
);
27 afterEach(function () {
31 describe('b64 armor', function () {
32 it('reciprocates', function () {
33 const src
= '3qmOlsGY5I3qY4O/tqF7LuvS400BBPh/AMzbyoIlvyXPwMA1tg==';
35 const armored
= Authenticator
.b64Armor(src
);
36 const unarmored
= Authenticator
.b64Unarmor(armored
);
38 assert
.strictEqual(unarmored
, src
);
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);
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
);
61 describe('requestSignature', function () {
64 beforeEach(function () {
66 setHeader: sinon
.stub(),
70 it('requests custom auth', function () {
72 authenticator
.requestSignature(res
);
73 assert
.fail(noExpectedException
);
75 assert(e
instanceof ResponseError
);
76 assert
.strictEqual(e
.statusCode
, 401);
77 assert(res
.setHeader
.called
);
80 }); // requestSignature
82 describe('isValidSignature', function () {
85 beforeEach(function () {
88 rawBody: 'all about werewolves',
90 sinon
.stub(authenticator
.db
, 'getAuthById').resolves({ secret: 'bluemoon' });
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');
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);
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);
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);
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);
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);
136 }); // isValidSignature
138 describe('isValidToken', function () {
141 beforeEach(function () {
143 linkId
= 'identifier';
144 sinon
.stub(authenticator
.db
, 'getLinkById');
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);
154 it('rejects missing token', async
function () {
155 const result
= await authenticator
.isValidToken(linkId
, token
);
156 assert
.strictEqual(result
, false);
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);
166 it('rejects missing id', async
function () {
167 token
= 'this_is_a_token';
169 authenticator
.db
.getLinkById
.resolves();
170 const result
= await authenticator
.isValidToken(linkId
, token
);
171 assert
.strictEqual(result
, false);
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);
181 describe('isValidBasic', function () {
182 let credentials
, ctx
;
184 beforeEach(function () {
185 sinon
.stub(authenticator
, 'requestBasic');
186 sinon
.stub(authenticator
.db
, 'getAuthById');
187 sinon
.stub(authenticator
.db
, 'upsertAuth');
188 credentials
= 'id:password';
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
);
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
);
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
);
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
);
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
);
233 describe('isValidBearer', function () {
234 let credentials
, ctx
;
236 beforeEach(function () {
237 credentials
= 'id:password';
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);
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);
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);
265 describe('isValidAuthorization', function () {
268 beforeEach(function () {
270 sinon
.stub(authenticator
, 'isValidBasic');
271 sinon
.stub(authenticator
, 'isValidBearer');
272 sinon
.stub(authenticator
, 'requestBasic');
275 it('dispatches basic', async
function () {
276 header
= 'Basic blahblahblah=';
277 await authenticator
.isValidAuthorization(header
, ctx
);
278 assert(authenticator
.isValidBasic
.called
);
281 it('dispatches bearer', async
function () {
282 header
= 'Bearer blahblahblah=';
283 await authenticator
.isValidAuthorization(header
, ctx
);
284 assert(authenticator
.isValidBearer
.called
);
287 it('handles fallback', async
function () {
288 header
= 'Digest blahblahblah=';
289 const result
= await authenticator
.isValidAuthorization(header
, ctx
);
290 assert
.strictEqual(result
, false);
292 }); // isValidAuthorization
294 describe('requestBasic', function () {
297 beforeEach(function () {
299 setHeader: sinon
.stub(),
303 it('requests custom auth', function () {
305 authenticator
.requestBasic(res
);
306 assert
.fail(noExpectedException
);
308 assert(e
instanceof ResponseError
);
309 assert
.strictEqual(e
.statusCode
, 401);
310 assert(res
.setHeader
.called
);
315 describe('required', function () {
318 beforeEach(function () {
320 getHeader: sinon
.stub(),
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');
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
);
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
);
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
);
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
);
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
);
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
);
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
);
388 it('fails missing token', async
function () {
389 ctx
.params
= { id: 'identifier' };
390 await authenticator
.required(req
, res
, ctx
);
391 assert(authenticator
.requestBasic
.called
);
394 it('requests basic when all else fails', async
function () {
395 await authenticator
.required(req
, res
, ctx
);
396 assert(authenticator
.requestBasic
.called
);
401 describe('optional', function () {
404 beforeEach(function () {
406 getHeader: sinon
.stub(),
410 sinon
.stub(authenticator
, 'isValidToken').resolves(false);
411 sinon
.stub(authenticator
, 'isValidSignature').resolves(false);
412 sinon
.stub(authenticator
, 'isValidAuthorization').resolves(false);
415 it('rejects with no auth', async
function () {
416 const result
= await authenticator
.optional(req
, res
, ctx
);
417 assert
.strictEqual(result
, false);
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);
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);
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);
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);
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);
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);
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);
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);
481 describe('signResponse', function () {
483 beforeEach(function () {
486 setHeader: sinon
.stub(),
490 it('does nothing without auth', function () {
491 authenticator
.signResponse(req
, res
, ctx
);
492 assert(!res
.setHeader
.called
);
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
);
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
);
507 it('covers big response non-logging', function () {
508 ctx
.responseBody
= 'orgle'.repeat(1000);
509 authenticator
.signResponse(req
, res
, ctx
);
511 it('covers buffer response', function () {
512 ctx
.responseBody
= Buffer
.from('orgle'.repeat(1000));
513 authenticator
.signResponse(req
, res
, ctx
);