--- /dev/null
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
+const Authenticator = require('../../lib/authenticator');
+const stubLogger = require('../stub-logger');
+const stubDb = require('../stub-db');
+const Errors = require('../../lib/errors');
+const Enum = require('../../lib/enum');
+const Config = require('../stub-config');
+
+const noExpectedException = 'did not receive expected exception';
+
+describe('Authenticator', function () {
+ let authenticator, credential, ctx, identifier, password, options;
+ function _authMechanismRequired(a, m) {
+ if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
+ this.skip();
+ }
+ };
+
+ beforeEach(function () {
+ options = Config('test');
+ authenticator = new Authenticator(stubLogger, stubDb, options);
+ identifier = 'username';
+ credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
+ ctx = {};
+ password = 'badPassword';
+ stubDb._reset();
+ });
+ afterEach(function () {
+ sinon.restore();
+ });
+
+ it('covers no auth mechanisms', function () {
+ options.authenticator.authnEnabled = [];
+ try {
+ authenticator = new Authenticator(stubLogger, stubDb, options);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.strictEqual(e.message, 'no authentication mechanisms available');
+ }
+ });
+
+ it('covers empty realm', function () {
+ options.authenticator.basicRealm = undefined;
+ authenticator = new Authenticator(stubLogger, stubDb, options);
+ });
+
+ describe('isValidBasic', function () {
+ it('succeeds', async function () {
+ _authMechanismRequired(authenticator, 'argon2');
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential,
+ });
+ const authString = `${identifier}:${password}`;
+ const result = await authenticator.isValidBasic(authString, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, identifier);
+ });
+ it('fails', async function () {
+ _authMechanismRequired(authenticator, 'argon2');
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential,
+ });
+ const authString = `${identifier}:wrongPassword}`;
+ const result = await authenticator.isValidBasic(authString, ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ it('covers no entry', async function() {
+ authenticator.db.authenticationGet.resolves();
+ const authString = `${identifier}:wrongPassword}`;
+ const result = await authenticator.isValidBasic(authString, ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ it('covers unknown password hash', async function () {
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential: '$other$kind_of_credential',
+ });
+ const authString = `${identifier}:wrongPassword}`;
+ const result = await authenticator.isValidBasic(authString, ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ }); // isValidBasic
+
+ describe('isValidIdentifierCredential', function () {
+ it('succeeds', async function () {
+ _authMechanismRequired(authenticator, 'argon2');
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential,
+ });
+ const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, identifier);
+ });
+ it('fails', async function () {
+ _authMechanismRequired(authenticator, 'argon2');
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential,
+ });
+ const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ it('covers no entry', async function() {
+ authenticator.db.authenticationGet.resolves();
+ const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ it('covers unknown password hash', async function () {
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential: '$other$kind_of_credential',
+ });
+ const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
+ it('covers PAM', async function () {
+ _authMechanismRequired(authenticator, 'pam');
+ sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential: '$PAM$',
+ });
+ const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, identifier);
+ });
+ it('covers debug', async function () {
+ authenticator.authnEnabled = ['DEBUG_ANY'];
+ const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, identifier);
+ });
+ }); // isValidIdentifierCredential
+
+ describe('_isValidPAMIdentifier', function () {
+ beforeEach(function () {
+ _authMechanismRequired(authenticator, 'pam');
+ sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
+ });
+ it('covers success', async function () {
+ authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
+ const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+ assert.strictEqual(result, true);
+ });
+ it('covers failure', async function () {
+ _authMechanismRequired(authenticator, 'pam');
+ authenticator.authn.pam.pamAuthenticatePromise.rejects(new authenticator.authn.pam.PamError());
+ const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+ assert.strictEqual(result, false);
+ });
+ it('covers error', async function () {
+ _authMechanismRequired(authenticator, 'pam');
+ const expected = new Error('blah');
+ authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
+ try {
+ await authenticator._isValidPAMIdentifier(identifier, credential);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.deepStrictEqual(e, expected);
+ }
+ });
+ it('covers forbidden', async function () {
+ identifier = 'root';
+ const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+ assert.strictEqual(result, false);
+ });
+ }); // _isValidPAMIdentifier
+
+ describe('_cookieParse', function () {
+ it('covers empty', function () {
+ const expected = {};
+ const result = Authenticator._cookieParse();
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers non variable', function () {
+ const cookie = 'foo';
+ const expected = {
+ foo: null,
+ };
+ const result = Authenticator._cookieParse(cookie);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('parses cookie', function () {
+ const cookie = 'foo=bar; baz="quux"';
+ const expected = {
+ foo: 'bar',
+ baz: 'quux',
+ };
+ const result = Authenticator._cookieParse(cookie);
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // _cookieParse
+
+ describe('isValidAuthorization', function () {
+ it('handles basic', async function () {
+ const expected = true;
+ const authorizationHeader = 'basic Zm9vOmJhcg==';
+ sinon.stub(authenticator, 'isValidBasic').resolves(expected);
+ const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
+ assert.strictEqual(result, expected);
+ });
+ it('handles other', async function () {
+ const expected = false;
+ const authorizationHeader = 'bearer Zm9vOmJhcg==';
+ const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
+ assert.strictEqual(result, expected);
+ });
+ }); // isValidAuthorization
+
+ describe('requestBasic', function () {
+ it('covers', function () {
+ try {
+ const res = {
+ setHeader: () => {},
+ };
+ authenticator.requestBasic(res);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof Errors.ResponseError);
+ assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
+ }
+ });
+ }); // requestBasic
+
+ describe('isValidCookieAuth', function () {
+ let cookie;
+ beforeEach(function () {
+ sinon.stub(authenticator.mysteryBox, 'unpack');
+ cookie = 'squeepSession=dummy';
+ });
+ it('covers identifier success', async function () {
+ authenticator.mysteryBox.unpack.resolves({
+ authenticatedIdentifier: 'identifier',
+ });
+ const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ assert.strictEqual(result, true);
+ });
+ it('covers profile success', async function () {
+ authenticator.mysteryBox.unpack.resolves({
+ authenticatedProfile: 'profile',
+ });
+ const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ assert.strictEqual(result, true);
+ });
+ it('covers missing cookie', async function () {
+ cookie = 'wrong=cookie';
+ const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ assert.strictEqual(result, false);
+ });
+ it('covers bad cookie', async function () {
+ authenticator.mysteryBox.unpack.rejects();
+ const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ assert.strictEqual(result, false);
+ });
+ it('covers broken session', async function () {
+ authenticator.mysteryBox.unpack.resolves({
+ randomData: 'foo',
+ });
+ const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ assert.strictEqual(result, false);
+ });
+ }); // isValidCookieAuth
+
+ describe('sessionCheck', function () {
+ let cookie, req, res, loginPath, required, profilesAllowed;
+ beforeEach(function () {
+ cookie = 'squeepSession=sessionCookie';
+ ctx.clientProtocol = 'https';
+ req = {
+ getHeader: sinon.stub(),
+ };
+ res = {
+ end: sinon.stub(),
+ setHeader: sinon.stub(),
+ };
+ loginPath = '/admin/login';
+ required = true;
+ profilesAllowed = true;
+ });
+ it('covers valid cookie session', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedIdentifier: 'user',
+ };
+ const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+ assert.strictEqual(result, true);
+ });
+ it('rejects insecure connection', async function () {
+ ctx.clientProtocol = 'http';
+ try {
+ await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof Errors.ResponseError);
+ assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
+ }
+ });
+ it('ignores insecure connection if auth not required', async function () {
+ ctx.clientProtocol = 'http';
+ required = false;
+ const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+ assert.strictEqual(result, false);
+ });
+ it('redirects without any auth', async function () {
+ await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+ assert(res.end.called);
+ assert(res.setHeader.called);
+ });
+ describe('convenience wrappers', function () {
+ describe('sessionRequiredLocal', function () {
+ it('accepts identifier', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedIdentifier: 'user',
+ };
+ const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
+ assert.strictEqual(result, true);
+ });
+ it('redirects with profile', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedProfile: 'user',
+ };
+ const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
+ assert.strictEqual(result, false);
+ assert(res.end.called);
+ assert(res.setHeader.called);
+ });
+ }); // sessionRequiredLocal
+ describe('sessionRequired', function () {
+ it('accepts identifier', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedIdentifier: 'user',
+ };
+ const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+ assert.strictEqual(result, true);
+ });
+ it('accepts profile', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedProfile: 'user',
+ };
+ const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+ assert.strictEqual(result, true);
+ });
+ it('rejects invalid', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
+ const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+ assert.strictEqual(result, false);
+ assert(res.end.called);
+ assert(res.setHeader.called);
+ });
+ it('covers insecure allowed', async function () {
+ authenticator.options.authenticator.secureAuthOnly = false;
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
+ const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+ assert.strictEqual(result, false);
+ assert(res.end.called);
+ assert(res.setHeader.called);
+ });
+ }); // sessionRequired
+ describe('sessionOptionalLocal', function () {
+ it('rejects profile', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+ ctx.session = {
+ authenticatedProfile: 'user',
+ };
+ const result = await authenticator.sessionOptionalLocal(req, res, ctx, loginPath);
+ assert.strictEqual(result, false);
+ assert(!res.end.called);
+ assert(!res.setHeader.called);
+ });
+ }); // sessionOptionalLocal
+ describe('sessionOptional', function () {
+ it('rejects invalid', async function () {
+ req.getHeader.returns(cookie);
+ sinon.stub(authenticator, 'isValidCookieAuth').resolves(false);
+ const result = await authenticator.sessionOptional(req, res, ctx, loginPath);
+ assert.strictEqual(result, false);
+ assert(!res.end.called);
+ assert(!res.setHeader.called);
+ });
+ }); // sessionOptional
+ }); // convenience wrappers
+ }); // sessionCheck
+
+}); // Authenticator