/* eslint-env mocha */
+/* eslint-disable sonarjs/no-duplicate-string */
+/* eslint-disable jsdoc/require-jsdoc */
'use strict';
-const assert = require('assert');
-const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
+const assert = require('node:assert');
+const sinon = require('sinon');
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');
ctx = {};
password = 'badPassword';
stubDb._reset();
+ stubLogger._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');
- }
+ assert.throws(() => new Authenticator(stubLogger, stubDb, options), {
+ message: 'no authentication mechanisms available',
+ });
});
it('covers empty realm', function () {
authenticator = new Authenticator(stubLogger, stubDb, options);
});
+ it('covers option defaults', function () {
+ delete options.authenticator.secureAuthOnly;
+ delete options.dingus?.proxyPrefix;
+ delete options.authenticator.forbiddenPAMIdentifiers;
+ options.authenticator.authnEnabled.push('flarpyauth');
+ authenticator = new Authenticator(stubLogger, stubDb, options);
+ });
+
+ describe('createIdentifier', function () {
+ let dbCtx;
+ beforeEach(function () {
+ dbCtx = {};
+ credential = 'badpassword';
+ });
+ it('covers success', async function () {
+ const otpKey = '1234567890123456789012';
+ await authenticator.createIdentifier(dbCtx, identifier, credential, otpKey);
+ assert(authenticator.db.authenticationUpsert.called);
+ });
+ it('covers failure', async function () {
+ const expected = new Error('blah');
+ await authenticator.db.authenticationUpsert.rejects(expected);
+ // assert.rejects was not happy to handle this for some reason
+ try {
+ await authenticator.createIdentifier(dbCtx, identifier, credential);
+ assert.fail('no expecte exception');
+ } catch (e) {
+ assert.deepStrictEqual(e, expected);
+ assert(authenticator.db.authenticationUpsert.called);
+ assert(authenticator.logger.error.called);
+ }
+ });
+ }); // createIdentifier
+
+ describe('updateCredential', function () {
+ let dbCtx, newCredential;
+ beforeEach(function () {
+ dbCtx = {};
+ newCredential = 'newpassword';
+ });
+ it('covers success', async function () {
+ await authenticator.updateCredential(dbCtx, identifier, newCredential);
+ assert(authenticator.db.authenticationUpdateCredential.called);
+ assert(authenticator.logger.info.called);
+ });
+ it('covers failure', async function () {
+ const expected = new Error('foo');
+ authenticator.db.authenticationUpdateCredential.rejects(expected);
+ try {
+ await authenticator.updateCredential(dbCtx, identifier, newCredential);
+ assert.fail('no expected exception');
+ } catch (e) {
+ assert.deepStrictEqual(e, expected);
+ assert(authenticator.logger.error.called);
+ }
+ // assert.rejects was not happy to handle this for some reason
+ });
+ }); // updateCredential
+
+ describe('_secureCredential', function () {
+ beforeEach(function () {
+ credential = 'badpassword';
+ });
+ it('covers plain', async function () {
+ const result = await authenticator._secureCredential(credential, 'plain');
+ assert.strictEqual(result, '$plain$' + credential);
+ });
+ it('covers default (argon2)', async function () {
+ const result = await authenticator._secureCredential(credential);
+ assert(result.startsWith('$argon2'));
+ });
+ it('covers invalid authn', async function () {
+ const authn = 'bogus';
+ assert.rejects(async () => await authenticator._secureCredential(credential, authn), RangeError);
+ });
+ }); // _secureCredential
+
+ describe('_validateAuthDataCredential', function () {
+ let authData;
+ beforeEach(function () {
+ credential = 'badpassword';
+ authData = {};
+ });
+ it('fails if not provided a credential', async function () {
+ const result = await authenticator._validateAuthDataCredential(authData, credential);
+ assert.strictEqual(result, false);
+ });
+ it('covers plain', async function () {
+ authData.credential = '$plain$badpassword';
+ const result = await authenticator._validateAuthDataCredential(authData, credential);
+ assert.strictEqual(result, true);
+ });
+ }); // _validateAuthDataCredential
+
describe('isValidBasic', function () {
+ const b64 = (x) => Buffer.from(x).toString('base64');
it('succeeds', async function () {
_authMechanismRequired(authenticator, 'argon2');
authenticator.db.authenticationGet.resolves({
identifier,
credential,
});
- const authString = `${identifier}:${password}`;
+ const authString = b64(`${identifier}:${password}`);
const result = await authenticator.isValidBasic(authString, ctx);
assert.strictEqual(result, true);
assert.strictEqual(ctx.authenticationId, identifier);
identifier,
credential,
});
- const authString = `${identifier}:wrongPassword}`;
+ const authString = b64(`${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 authString = b64(`${identifier}:wrongPassword}`);
const result = await authenticator.isValidBasic(authString, ctx);
assert.strictEqual(result, false);
assert.strictEqual(ctx.authenticationId, undefined);
identifier,
credential: '$other$kind_of_credential',
});
- const authString = `${identifier}:wrongPassword}`;
+ const authString = b64(`${identifier}:wrongPassword}`);
const result = await authenticator.isValidBasic(authString, ctx);
assert.strictEqual(result, false);
assert.strictEqual(ctx.authenticationId, undefined);
assert.strictEqual(result, true);
assert.strictEqual(ctx.authenticationId, identifier);
});
+ it('succeeds with OTP', async function () {
+ const otpKey = Buffer.from('1234567890');
+ _authMechanismRequired(authenticator, 'argon2');
+ authenticator.db.authenticationGet.resolves({
+ identifier,
+ credential,
+ otpKey,
+ });
+ const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, identifier);
+ assert.deepStrictEqual(ctx.otpKey, otpKey);
+ });
it('fails', async function () {
_authMechanismRequired(authenticator, 'argon2');
authenticator.db.authenticationGet.resolves({
assert.strictEqual(result, false);
assert.strictEqual(ctx.authenticationId, undefined);
});
+ it('covers non-string credential', async function () {
+ credential = '$argon2id$v=19$m=4096,t=3,p=1$SbAlHo5x2HM0PvMAWYHqww$gNn/o+B6+IWsnrVupPkTAiiK9tvwV+eM/HoXG41bnzM';
+ const result = await authenticator.isValidIdentifierCredential(identifier, undefined, ctx);
+ assert.strictEqual(result, false);
+ assert.strictEqual(ctx.authenticationId, undefined);
+ });
it('covers unknown password hash', async function () {
authenticator.db.authenticationGet.resolves({
identifier,
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 () {
+ let authData;
beforeEach(function () {
_authMechanismRequired(authenticator, 'pam');
sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
+ authData = {
+ identifier,
+ };
});
it('covers success', async function () {
authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
- const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+ const result = await authenticator._isValidPAMIdentifier(authData, 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);
+ const result = await authenticator._isValidPAMIdentifier(authData, 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);
- }
+ assert.rejects(() => authenticator._isValidPAMIdentifier(authData, credential), expected);
});
it('covers forbidden', async function () {
- identifier = 'root';
- const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+ authData.identifier = 'root';
+ const result = await authenticator._isValidPAMIdentifier(authData, 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;
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);
- }
+ const res = {
+ setHeader: () => {},
+ };
+ assert.throws(() => authenticator.requestBasic(res), {
+ name: 'ResponseError',
+ statusCode: Enum.ErrorResponse.Unauthorized.statusCode,
+ });
});
}); // requestBasic
describe('isValidCookieAuth', function () {
- let cookie;
beforeEach(function () {
sinon.stub(authenticator.mysteryBox, 'unpack');
- cookie = 'squeepSession=dummy';
+ ctx.cookie = {
+ squeepSession: 'dummy',
+ otherCookie: 'foo',
+ };
});
it('covers identifier success', async function () {
authenticator.mysteryBox.unpack.resolves({
authenticatedIdentifier: 'identifier',
});
- const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ const result = await authenticator.isValidCookieAuth(ctx);
assert.strictEqual(result, true);
});
it('covers profile success', async function () {
authenticator.mysteryBox.unpack.resolves({
authenticatedProfile: 'profile',
});
- const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ const result = await authenticator.isValidCookieAuth(ctx);
assert.strictEqual(result, true);
});
it('covers missing cookie', async function () {
- cookie = 'wrong=cookie';
- const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ delete ctx.cookie.squeepSession;
+ const result = await authenticator.isValidCookieAuth(ctx);
assert.strictEqual(result, false);
});
it('covers bad cookie', async function () {
authenticator.mysteryBox.unpack.rejects();
- const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ const result = await authenticator.isValidCookieAuth(ctx);
assert.strictEqual(result, false);
});
it('covers broken session', async function () {
authenticator.mysteryBox.unpack.resolves({
randomData: 'foo',
});
- const result = await authenticator.isValidCookieAuth(ctx, cookie);
+ const result = await authenticator.isValidCookieAuth(ctx);
assert.strictEqual(result, false);
});
}); // isValidCookieAuth
+ describe('checkOTP', function () {
+ let state, otp;
+ this.beforeEach(function () {
+ sinon.stub(authenticator.TOTP.prototype, 'validate').returns(true);
+ state = {
+ key: '12345678901234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ };
+ otp = '000000';
+ });
+ it('covers valid OTP entry', function () {
+ const result = authenticator.checkOTP(state, otp);
+ assert.strictEqual(result, Enum.OTPResult.Valid);
+ });
+ it('covers invalid OTP entry', function () {
+ authenticator.TOTP.prototype.validate.returns(false);
+ const result = authenticator.checkOTP(state, otp);
+ assert.strictEqual(result, Enum.OTPResult.InvalidSoftFail);
+ });
+ it('covers invalid OTP entry, too many failures', function () {
+ state.attempt = 10;
+ authenticator.TOTP.prototype.validate.returns(false);
+ const result = authenticator.checkOTP(state, otp);
+ assert.strictEqual(result, Enum.OTPResult.InvalidHardFail);
+ });
+ it('covers invalid OTP entry', function () {
+ state.epochMs = 0;
+ authenticator.TOTP.prototype.validate.returns(false);
+ const result = authenticator.checkOTP(state, otp);
+ assert.strictEqual(result, Enum.OTPResult.InvalidHardFail);
+ });
+ }); // checkOTP
+
+ describe('updateOTPKey', function () {
+ let dbCtx, otpKey;
+ beforeEach(function () {
+ dbCtx = {};
+ otpKey = 'CDBGB3U3B2ILECQORMINGGSZN7LXY565';
+ });
+ it('covers success', async function () {
+ await authenticator.updateOTPKey(dbCtx, identifier, otpKey);
+ assert(authenticator.db.authenticationUpdateOTPKey.called);
+ });
+ it('covers failure', async function () {
+ authenticator.db.authenticationUpdateOTPKey.rejects();
+ assert.rejects(authenticator.updateOTPKey(dbCtx, identifier, otpKey));
+ });
+ }); // updateOTPKey
+
describe('sessionCheck', function () {
- let cookie, req, res, loginPath, required, profilesAllowed;
+ let req, res, loginPath, required, profilesAllowed;
beforeEach(function () {
- cookie = 'squeepSession=sessionCookie';
ctx.clientProtocol = 'https';
+ ctx.cookie = {
+ squeepSession: 'squeep_session_blob',
+ };
req = {
getHeader: sinon.stub(),
};
res = {
end: sinon.stub(),
setHeader: sinon.stub(),
+ appendHeader: 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('covers valid insecure cookie session', async function () {
+ authenticator.secureAuthOnly = false;
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedIdentifier: 'user',
});
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);
- }
+ assert.rejects(() => authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed), {
+ name: 'ResponseError',
+ sttausCode: Enum.ErrorResponse.Forbidden.statusCode,
+ });
});
it('ignores insecure connection if auth not required', async function () {
ctx.clientProtocol = 'http';
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',
assert.strictEqual(result, true);
});
it('redirects with profile', async function () {
- req.getHeader.returns(cookie);
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedProfile: 'user',
}); // sessionRequiredLocal
describe('sessionRequired', function () {
it('accepts identifier', async function () {
- req.getHeader.returns(cookie);
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedIdentifier: 'user',
assert.strictEqual(result, true);
});
it('accepts profile', async function () {
- req.getHeader.returns(cookie);
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedProfile: 'user',
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);
});
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);
}); // sessionRequired
describe('sessionOptionalLocal', function () {
it('rejects profile', async function () {
- req.getHeader.returns(cookie);
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedProfile: 'user',
}); // 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);
}); // sessionCheck
describe('apiRequiredLocal', function () {
- let req, res, ctx;
+ let req, res;
beforeEach(function () {
ctx = {};
req = {
sinon.stub(authenticator, 'sessionCheck').resolves(false);
sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
const result = await authenticator.apiRequiredLocal(req, res, ctx);
- assert(authenticator.sessionCheck.called);
+ assert.strictEqual(result, true);
+ assert(authenticator.isValidAuthorization.called);
+ assert(!authenticator.sessionCheck.called);
+ });
+ it('covers invalid basic auth', async function () {
+ req.getHeader.returns('Basic Zm9vOmJhcg==');
+ sinon.stub(authenticator, 'sessionCheck').resolves(false);
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
+ assert.rejects(() => authenticator.apiRequiredLocal(req, res, ctx), {
+ name: 'ResponseError',
+ statusCode: 401,
+ });
+ assert(!authenticator.sessionCheck.called);
assert(authenticator.isValidAuthorization.called);
+ });
+ it('covers missing basic auth, valid session', async function () {
+ req.getHeader.returns();
+ sinon.stub(authenticator, 'sessionCheck').resolves(true);
+ sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
+ const result = await authenticator.apiRequiredLocal(req, res, ctx);
assert.strictEqual(result, true);
+ assert(!authenticator.isValidAuthorization.called);
+ assert(authenticator.sessionCheck.called);
});
- it('requests basic auth when missing, ignores session', async function () {
+ it('covers missing basic auth, ignores session', async function () {
req.getHeader.returns();
sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
- try {
- await authenticator.apiRequiredLocal(req, res, ctx, false);
- assert.fail(noExpectedException);
- } catch (e) {
- console.log(e);
- assert.strictEqual(e.statusCode, 401);
- assert(!authenticator.sessionCheck.called);
- assert(!authenticator.isValidAuthorization.called);
- assert(res.setHeader.called);
- }
+ assert.rejects(() => authenticator.apiRequiredLocal(req, res, ctx, false), {
+ name: 'ResponseError',
+ statusCode: 401,
+ });
+ assert(!authenticator.sessionCheck.called);
+ assert(!authenticator.isValidAuthorization.called);
+ assert(res.setHeader.called);
+ });
+ it('covers errors', async function () {
+ sinon.stub(authenticator, 'isValidAuthorization').rejects();
+ req.getHeader.returns('Basic Zm9vOmJhcg==');
+ assert.rejects(() => authenticator.apiRequiredLocal(req, res, ctx));
});
}); // apiRequiredLocal