/* 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');
ctx = {};
password = 'badPassword';
stubDb._reset();
+ stubLogger._reset();
});
afterEach(function () {
sinon.restore();
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, 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);
- assert.rejects(() => authenticator._isValidPAMIdentifier(identifier, credential), 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;
}); // 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',
});
it('covers valid insecure cookie session', async function () {
authenticator.secureAuthOnly = false;
- req.getHeader.returns(cookie);
sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
ctx.session = {
authenticatedIdentifier: 'user',
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);
it('covers missing basic auth, ignores session', async function () {
req.getHeader.returns();
sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
- assert.rejects(authenticator.apiRequiredLocal(req, res, ctx, false), {
+ assert.rejects(() => authenticator.apiRequiredLocal(req, res, ctx, false), {
name: 'ResponseError',
statusCode: 401,
});
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
}); // Authenticator