+/* eslint-env mocha */
+/* eslint-disable capitalized-comments */
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon');
+
+const Authenticator = require('../../src/authenticator');
+const { Errors: { ResponseError } } = require('@squeep/api-dingus');
+
+
+const noExpectedException = 'did not get expected exception';
+
+describe('Authenticator', function () {
+ let authenticator, logger, db, options;
+
+ beforeEach(function () {
+ logger = { debug: () => {} };
+ // logger = console;
+ db = {
+ context: async (fn) => fn({}),
+ getAuthById: async () => {},
+ getLinkById: async () => {},
+ };
+ authenticator = new Authenticator(logger, db, options);
+ });
+
+ afterEach(function () {
+ sinon.restore();
+ });
+
+ describe('b64 armor', function () {
+ it('reciprocates', function () {
+ const src = '3qmOlsGY5I3qY4O/tqF7LuvS400BBPh/AMzbyoIlvyXPwMA1tg==';
+
+ const armored = Authenticator.b64Armor(src);
+ const unarmored = Authenticator.b64Unarmor(armored);
+
+ assert.strictEqual(unarmored, src);
+ });
+ }); // b64 armor
+
+ describe('generateToken', function () {
+ it('generates a token', async function () {
+ const result = await Authenticator.generateToken();
+ assert.strictEqual(result.length, 36 * 4 / 3);
+ });
+ }); // generateToken
+
+ describe('signature', function () {
+ it('generates a signature', async function () {
+ const secret = 'secret';
+ const id = 'identifier';
+ const epoch = '1602887798';
+ const body = 'some data';
+ const expected = 'P5IXYXu5a7aobQvRinPQlN_k1g8GHycRpx3JrK1O7YJlqmhv3WRP5M3ubObPdUWM';
+ const result = await Authenticator.signature(secret, id, epoch, body);
+ assert.strictEqual(result, expected);
+ });
+ }); // generateToken
+
+ describe('requestSignature', function () {
+ let res;
+
+ beforeEach(function () {
+ res = {
+ setHeader: sinon.stub(),
+ };
+ });
+
+ it('requests custom auth', function () {
+ try {
+ authenticator.requestSignature(res);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof ResponseError);
+ assert.strictEqual(e.statusCode, 401);
+ assert(res.setHeader.called);
+ }
+ });
+ }); // requestSignature
+
+ describe('isValidSignature', function () {
+ let reqSig, ctx;
+
+ beforeEach(function () {
+ reqSig = undefined;
+ ctx = {
+ rawBody: 'all about werewolves',
+ };
+ sinon.stub(authenticator.db, 'getAuthById').resolves({ secret: 'bluemoon' });
+ });
+
+ it('passes valid signature', async function () {
+ reqSig = 'awoo:1604155860:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, 'awoo');
+ });
+
+ it('fails invalid data', async function () {
+ reqSig = 'not a signature';
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('fails missing data', async function () {
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('fails invalid signature', async function () {
+ reqSig = 'awoo:1604155860:bad signature';
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('fails invalid timestamp', async function () {
+ reqSig = 'awoo:0:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('fails invalid auth id', async function () {
+ reqSig = 'awoo:1604155860:Zz43TbCdVeqRAqhxTnVpTcC2aMdh3PLXNw_FuQ4KVhjsEFTEnpokwTHb-Qbt3ttz';
+ authenticator.db.getAuthById.restore();
+ sinon.stub(authenticator.db, 'getAuthById').resolves({});
+ sinon.stub(Date, 'now').returns(1604155861 * 1000);
+ const result = await authenticator.isValidSignature(reqSig, ctx);
+ assert.strictEqual(result, false);
+ });
+ }); // isValidSignature
+
+ describe('isValidToken', function () {
+ let token, linkId;
+
+ beforeEach(function () {
+ token = undefined;
+ linkId = 'identifier';
+ sinon.stub(authenticator.db, 'getLinkById');
+ });
+
+ it('accepts token', async function () {
+ token = 'this_is_a_token';
+ authenticator.db.getLinkById.resolves({ authToken: token });
+ const result = await authenticator.isValidToken(linkId, token);
+ assert.strictEqual(result, true);
+ });
+
+ it('rejects missing token', async function () {
+ const result = await authenticator.isValidToken(linkId, token);
+ assert.strictEqual(result, false);
+ });
+
+ it('rejects wrong token', async function () {
+ token = 'this_is_a_token';
+ authenticator.db.getLinkById.resolves({ authToken: 'some_other_token' });
+ const result = await authenticator.isValidToken(linkId, token);
+ assert.strictEqual(result, false);
+ });
+
+ it('rejects missing id', async function () {
+ token = 'this_is_a_token';
+ linkId = undefined;
+ authenticator.db.getLinkById.resolves();
+ const result = await authenticator.isValidToken(linkId, token);
+ assert.strictEqual(result, false);
+ });
+
+ it('rejects invalid id', async function () {
+ token = 'this_is_a_token';
+ const result = await authenticator.isValidToken(linkId, token);
+ assert.strictEqual(result, false);
+ });
+ }); // isValidToken
+
+ describe('isValidBasic', function () {
+ let credentials, ctx;
+
+ beforeEach(function () {
+ sinon.stub(authenticator, 'requestBasic');
+ credentials = 'id:password';
+ ctx = {};
+ });
+
+ it('accepts credentials', async function () {
+ sinon.stub(authenticator.db, 'getAuthById').resolves({ password: 'password' });
+ const result = await authenticator.isValidBasic(credentials, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, 'id');
+ });
+
+ it('rejects wrong password', async function () {
+ sinon.stub(authenticator.db, 'getAuthById').resolves({ password: 'wrong_password' });
+ const result = await authenticator.isValidBasic(credentials, ctx);
+ assert.strictEqual(result, false);
+ assert(!('authenticationId' in ctx));
+ });
+
+ it('rejects missing id', async function () {
+ sinon.stub(authenticator.db, 'getAuthById').resolves();
+ const result = await authenticator.isValidBasic(credentials, ctx);
+ assert.strictEqual(result, false);
+ assert(!('authenticationId' in ctx));
+ });
+ }); // isValidBasic
+
+ describe('isValidBearer', function () {
+ let credentials, ctx;
+
+ beforeEach(function () {
+ credentials = 'id:password';
+ ctx = {
+ params: {},
+ };
+ });
+
+ it('accepts token', async function () {
+ ctx.params.id = 'identifier';
+ credentials = 'token';
+ sinon.stub(authenticator.db, 'getLinkById').resolves({ authToken: 'token' });
+ const result = await authenticator.isValidBearer(credentials, ctx);
+ assert.strictEqual(result, true);
+ });
+
+ it('rejects wrong token', async function () {
+ ctx.params.id = 'identifier';
+ sinon.stub(authenticator.db, 'getLinkById').resolves({ authToken: 'wrong_token' });
+ const result = await authenticator.isValidBearer(credentials, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('rejects missing id', async function () {
+ sinon.stub(authenticator.db, 'getLinkById').resolves();
+ const result = await authenticator.isValidBearer(credentials, ctx);
+ assert.strictEqual(result, false);
+ });
+ }); // isValidBearer
+
+ describe('isValidAuthorization', function () {
+ let header, ctx;
+
+ beforeEach(function () {
+ header = undefined;
+ sinon.stub(authenticator, 'isValidBasic');
+ sinon.stub(authenticator, 'isValidBearer');
+ sinon.stub(authenticator, 'requestBasic');
+ });
+
+ it('dispatches basic', async function () {
+ header = 'Basic blahblahblah=';
+ await authenticator.isValidAuthorization(header, ctx);
+ assert(authenticator.isValidBasic.called);
+ });
+
+ it('dispatches bearer', async function () {
+ header = 'Bearer blahblahblah=';
+ await authenticator.isValidAuthorization(header, ctx);
+ assert(authenticator.isValidBearer.called);
+ });
+
+ it('handles fallback', async function () {
+ header = 'Digest blahblahblah=';
+ const result = await authenticator.isValidAuthorization(header, ctx);
+ assert.strictEqual(result, false);
+ });
+ }); // isValidAuthorization
+
+ describe('requestBasic', function () {
+ let res;
+
+ beforeEach(function () {
+ res = {
+ setHeader: sinon.stub(),
+ };
+ });
+
+ it('requests custom auth', function () {
+ try {
+ authenticator.requestBasic(res);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof ResponseError);
+ assert.strictEqual(e.statusCode, 401);
+ assert(res.setHeader.called);
+ }
+ });
+ }); // requestBasic
+
+ describe('required', function () {
+ let req, res, ctx;
+
+ beforeEach(function () {
+ req = {
+ getHeader: sinon.stub(),
+ };
+ res = {};
+ ctx = {};
+ sinon.stub(authenticator, 'isValidToken').resolves(false);
+ sinon.stub(authenticator, 'isValidSignature').resolves(false);
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
+ sinon.stub(authenticator, 'requestBasic');
+ sinon.stub(authenticator, 'requestSignature');
+ });
+
+ it('validates signature auth', async function () {
+ req.getHeader.returns('signature');
+ authenticator.isValidSignature.restore();
+ sinon.stub(authenticator, 'isValidSignature').resolves(true);
+ const result = await authenticator.required(req, res, ctx);
+ assert.strictEqual(result, true);
+ assert(!authenticator.requestBasic.called);
+ });
+
+ it('requests signature auth on signature failure', async function () {
+ req.getHeader.returns('signature');
+ await authenticator.required(req, res, ctx);
+ assert(authenticator.requestSignature.called);
+ });
+
+ it('validates authorization auth', async function () {
+ req.getHeader.onCall(1).returns('signature');
+ authenticator.isValidAuthorization.restore();
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
+ const result = await authenticator.required(req, res, ctx);
+ assert.strictEqual(result, true);
+ assert(!authenticator.requestBasic.called);
+ });
+
+ it('requests authorization on auth failure', async function () {
+ req.getHeader.onCall(1).returns('signature');
+ await authenticator.required(req, res, ctx);
+ assert(authenticator.requestBasic.called);
+ });
+
+ it('validates a queryparams token', async function () {
+ ctx.params = { id: 'identifier' };
+ ctx.queryParams = { token: 'token' };
+ authenticator.isValidToken.restore();
+ sinon.stub(authenticator, 'isValidToken').resolves(true);
+ const result = await authenticator.required(req, res, ctx);
+ assert.strictEqual(result, true);
+ assert(!authenticator.requestBasic.called);
+ });
+
+ it('validates a body token', async function () {
+ ctx.params = { id: 'identifier' };
+ ctx.parsedBody = { token: 'token' };
+ authenticator.isValidToken.restore();
+ sinon.stub(authenticator, 'isValidToken').resolves(true);
+ const result = await authenticator.required(req, res, ctx);
+ assert.strictEqual(result, true);
+ assert(!authenticator.requestBasic.called);
+ });
+
+ it('fails invalid token', async function () {
+ ctx.params = { id: 'identifier' };
+ ctx.parsedBody = { token: 'token' };
+ await authenticator.required(req, res, ctx);
+ assert(authenticator.requestBasic.called);
+ });
+
+ it('fails missing token', async function () {
+ ctx.params = { id: 'identifier' };
+ await authenticator.required(req, res, ctx);
+ assert(authenticator.requestBasic.called);
+ });
+
+ it('requests basic when all else fails', async function () {
+ await authenticator.required(req, res, ctx);
+ assert(authenticator.requestBasic.called);
+ });
+
+ }); // required
+
+ describe('optional', function () {
+ let req, res, ctx;
+
+ beforeEach(function () {
+ req = {
+ getHeader: sinon.stub(),
+ };
+ res = {};
+ ctx = {};
+ sinon.stub(authenticator, 'isValidToken').resolves(false);
+ sinon.stub(authenticator, 'isValidSignature').resolves(false);
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
+ });
+
+ it('rejects with no auth', async function () {
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('validates signature auth', async function () {
+ req.getHeader.onCall(0).returns('signature');
+ authenticator.isValidSignature.restore();
+ sinon.stub(authenticator, 'isValidSignature').resolves(true);
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, true);
+ });
+
+ it('rejects invalid signature auth', async function () {
+ req.getHeader.onCall(0).returns('signature');
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('validates auth', async function () {
+ req.getHeader.onCall(1).returns('basic auth');
+ authenticator.isValidAuthorization.restore();
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, true);
+ });
+
+ it('rejects invalid auth', async function () {
+ req.getHeader.onCall(1).returns('basic auth');
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('validates queryparam token', async function () {
+ ctx.queryParams = { token: 'token' };
+ ctx.params = { id: 'identifier' };
+ authenticator.isValidToken.restore();
+ sinon.stub(authenticator, 'isValidToken').resolves(true);
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, true);
+ });
+
+ it('validates body token', async function () {
+ ctx.parsedBody = { token: 'token' };
+ ctx.params = { id: 'identifier' };
+ authenticator.isValidToken.restore();
+ sinon.stub(authenticator, 'isValidToken').resolves(true);
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, true);
+ });
+
+ it('rejects invalid token', async function () {
+ ctx.queryParams = { token: 'token' };
+ ctx.params = { id: 'identifier' };
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ it('rejects missing token', async function () {
+ ctx.params = { id: 'identifier' };
+ const result = await authenticator.optional(req, res, ctx);
+ assert.strictEqual(result, false);
+ });
+
+ }); // optional
+
+ describe('signResponse', function () {
+ let req, res, ctx;
+ beforeEach(function () {
+ req = {};
+ res = {
+ setHeader: sinon.stub(),
+ };
+ ctx = {};
+ });
+ it('does nothing without auth', function () {
+ authenticator.signResponse(req, res, ctx);
+ assert(!res.setHeader.called);
+ });
+ it('signs a response', function () {
+ ctx.authenticationId = 'identified';
+ ctx.authenticationSecret = 'secret';
+ ctx.responseBody = 'awoo';
+ authenticator.signResponse(req, res, ctx);
+ assert(res.setHeader.called);
+ });
+ it('signs an empty response', function () {
+ ctx.authenticationId = 'identified';
+ ctx.authenticationSecret = 'secret';
+ authenticator.signResponse(req, res, ctx);
+ assert(res.setHeader.called);
+ });
+ it('covers big response non-logging', function () {
+ ctx.responseBody = 'orgle'.repeat(1000);
+ authenticator.signResponse(req, res, ctx);
+ });
+ it('covers buffer response', function () {
+ ctx.responseBody = Buffer.from('orgle'.repeat(1000));
+ authenticator.signResponse(req, res, ctx);
+ });
+ }); // signResponse
+
+}); // Authenticator