const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const SessionManager = require('../../lib/session-manager');
+const Enum = require('../../lib/enum');
const Config = require('../stub-config');
const stubLogger = require('../stub-logger');
+const stubDb = require('../stub-db');
describe('SessionManager', function () {
let manager, options, stubAuthenticator;
let res, ctx;
beforeEach(function () {
+ stubDb._reset();
options = new Config('test');
res = {
end: sinon.stub(),
setHeader: sinon.stub(),
+ appendHeader: sinon.stub(),
};
ctx = {
cookie: '',
params: {},
queryParams: {},
parsedBody: {},
+ errors: [],
+ notifications: [],
};
stubAuthenticator = {
isValidIdentifierCredential: sinon.stub(),
+ checkOTP: sinon.stub(),
+ _validateAuthDataCredential: sinon.stub(),
+ updateCredential: sinon.stub(),
+ db: stubDb,
};
manager = new SessionManager(stubLogger, stubAuthenticator, options);
sinon.stub(manager.indieAuthCommunication);
sinon.restore();
});
+ describe('constructor', function () {
+ it('covers options', function () {
+ delete options.dingus.proxyPrefix;
+ manager = new SessionManager(stubLogger, stubAuthenticator, options);
+ });
+ }); // constructor
+
describe('_sessionCookieSet', function () {
let session, maxAge;
beforeEach(function () {
});
it('covers', async function () {
await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
+ assert(res.appendHeader.called);
});
it('covers reset', async function () {
session = undefined;
maxAge = 0;
await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
+ assert(res.appendHeader.called);
});
it('covers options', async function() {
options.authenticator.secureAuthOnly = false;
- await manager._sessionCookieSet(res, session, undefined, '');
- assert(res.setHeader.called);
+ await manager._sessionCookieSet(res, session, 'none', '');
+ assert(res.appendHeader.called);
});
}); // _sessionCookieSet
- describe('getAdminLogin', function () {
+ describe('_sessionCookieClear', function () {
it('covers', async function () {
+ await manager._sessionCookieClear(res);
+ assert(res.appendHeader.called);
+ })
+ }); // _sessionCookieClear
+
+ describe('getAdminLogin', function () {
+ it('covers no session', async function () {
+ await manager.getAdminLogin(res, ctx);
+ });
+ it('covers established session', async function () {
+ ctx.authenticationId = 'identifier';
+ ctx.queryParams['r'] = '/admin';
await manager.getAdminLogin(res, ctx);
+ assert.strictEqual(res.statusCode, 302);
+ assert(res.setHeader.called);
});
}); // getAdminLogin
describe('postAdminLogin', function () {
+ beforeEach(function () {
+ sinon.stub(manager, '_otpSubmission').resolves(false);
+ });
+ it('covers otp submission', async function () {
+ manager._otpSubmission.resolves(true);
+ await manager.postAdminLogin(res, ctx);
+ assert(res.end.notCalled);
+ });
it('covers valid local', async function () {
ctx.parsedBody.identifier = 'user';
ctx.parsedBody.credential = 'password';
});
it('covers profile scheme fallback', async function () {
ctx.parsedBody.me = 'https://example.com/profile';
- ctx.parsedBody.me_auto_scheme = '1';
+ ctx.parsedBody['me_auto_scheme'] = '1';
manager.indieAuthCommunication.fetchProfile
.onCall(0).resolves()
.onCall(1).resolves({
- metadata: {
- issuer: 'https://example.com/',
- authorizationEndpoint: 'https://example.com/auth',
- },
- });
+ metadata: {
+ issuer: 'https://example.com/',
+ authorizationEndpoint: 'https://example.com/auth',
+ },
+ });
await manager.postAdminLogin(res, ctx);
assert.strictEqual(res.statusCode, 302);
}); // living-standard-20220212
}); // postAdminLogin
+ describe('_otpSubmission', function () {
+ let otpState;
+ beforeEach(function () {
+ sinon.useFakeTimers(new Date());
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
+ manager.authenticator.checkOTP.resolves(Enum.OTPResult.Valid);
+ ctx.parsedBody.state = 'state_data';
+ ctx.parsedBody.otp = '123456';
+ });
+ it('returns false if no otp state', async function () {
+ delete ctx.parsedBody.state;
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.notCalled);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when presented with invalid otp state', async function () {
+ manager.mysteryBox.unpack.rejects();
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing identifier field', async function () {
+ delete otpState.authenticatedIdentifier;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing key field', async function () {
+ delete otpState.key;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing attempt field', async function () {
+ delete otpState.attempt;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing epoch field', async function () {
+ delete otpState.epochMs;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns true when submitted otp is invalid, but allowed to retry', async function () {
+ manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidSoftFail);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, true);
+ assert(res.end.called);
+ });
+ it('returns false when submitted otp is invalid and too many attempts', async function () {
+ otpState.attempt = 10;
+ manager.mysteryBox.unpack.resolves(otpState);
+ manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when submitted otp is invalid and too much time has passed', async function () {
+ otpState.epochMs = Date.now() - 99999999;
+ manager.mysteryBox.unpack.resolves(otpState);
+ manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns true when no otp submitted', async function () {
+ ctx.parsedBody.otp = '';
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, true);
+ assert(res.end.called);
+ });
+ it('returns true when submitted otp is valid', async function () {
+ const result = await manager._otpSubmission(res, ctx);
+ assert(res.end.called);
+ assert.strictEqual(result, true);
+ });
+ it('covers unexpected otp response', async function () {
+ manager.authenticator.checkOTP.resolves('wrong');
+ assert.rejects(() => manager._otpSubmission(res, ctx), RangeError);
+ });
+ }); // _otpSubmission
+
+ describe('_validateOTPState', function () {
+ let otpState;
+ it('covers valid', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ SessionManager._validateOTPState(otpState);
+ });
+ it('covers missing identifier', function () {
+ otpState = {
+ authenticatedIdentifier: '',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing key', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing attempt', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing epoch', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing redirect', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ }); // _validateOTPState
+
+ describe('_localUserAuth', function () {
+ beforeEach(function () {
+ ctx.parsedBody.identifier = 'identifier';
+ ctx.parsedBody.credential = 'credential';
+ manager.authenticator.isValidIdentifierCredential.resolves(true);
+ sinon.stub(manager.mysteryBox, 'pack').resolves('box');
+ });
+ it('returns false if indieauth available', async function () {
+ ctx.parsedBody.me = 'https://example.com/';
+ const result = await manager._localUserAuth(res, ctx);
+ assert.strictEqual(result, false);
+ });
+ it('returns true if identifier is invalid', async function () {
+ manager.authenticator.isValidIdentifierCredential.resolves(false);
+ const result = await manager._localUserAuth(res, ctx);
+ assert.strictEqual(result, true);
+ assert(manager.authenticator.isValidIdentifierCredential.called);
+ assert(res.end.called);
+ });
+ it('returns true if valid identifier', async function () {
+ const result = await manager._localUserAuth(res, ctx);
+ assert.strictEqual(result, true);
+ assert(res.end.called);
+ });
+ it('returns true if valid identifier requires otp entry', async function () {
+ ctx.otpKey = '1234567890123456789012';
+ const result = await manager._localUserAuth(res, ctx);
+ assert.strictEqual(result, true);
+ assert(manager.mysteryBox.pack.called);
+ assert(res.end.called);
+ });
+ }); // _localUserAuth
+
describe('getAdminLogout', function () {
it('covers', async function () {
await manager.getAdminLogout(res, ctx);
state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
me = 'https://example.com/profile';
authorizationEndpoint = 'https://example.com/auth'
- ctx.cookie = 'squeepSession=sessionCookie';
+ ctx.cookie = {
+ squeepSession: 'sessionCookie',
+ };
manager.indieAuthCommunication.redeemProfileCode.resolves({
me,
});
}); // living-standard-20220212
}); // getAdminIA
-}); // SessionManager
\ No newline at end of file
+ describe('getAdminSettings', function () {
+ it('covers success', async function () {
+ manager.db.authenticationGet.resolves({});
+ await manager.getAdminSettings(res, ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers no user', async function () {
+ manager.db.authenticationGet.resolves();
+ await manager.getAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.db.authenticationGet.throws();
+ await manager.getAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ }); // getAdminSettings
+
+ describe('postAdminSettings', function () {
+ let authData;
+ beforeEach(function () {
+ authData = {
+ identifier: 'user',
+ credential: 'password',
+ otpKey: '12345678901234567890123456789012',
+ };
+ manager.db.authenticationGet.resolves(authData);
+ sinon.stub(manager, '_credentialUpdate');
+ sinon.stub(manager, '_otpEnable');
+ sinon.stub(manager, '_otpConfirm');
+ sinon.stub(manager, '_otpDisable');
+ });
+ it('covers no action', async function () {
+ await manager.postAdminSettings(res, ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers db empty', async function () {
+ manager.db.authenticationGet.resolves();
+ await manager.postAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers db error', async function () {
+ manager.db.authenticationGet.throws();
+ await manager.postAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers credential update', async function () {
+ ctx.parsedBody.credential = 'update';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._credentialUpdate.called);
+ });
+ it('covers otp enabling', async function () {
+ ctx.parsedBody.otp = 'enable';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpEnable.called);
+ });
+ it('covers otp confirmation', async function () {
+ ctx.parsedBody.otp = 'confirm';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpConfirm.called);
+ });
+ it('covers otp disabling', async function () {
+ ctx.parsedBody.otp = 'disable';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpDisable.called);
+ });
+ }); // postAdminSettings
+
+ describe('_otpDisable', function () {
+ let dbCtx, authData;
+ beforeEach(function () {
+ ctx.otpKey = '12345678901234567890123456789012';
+ dbCtx = {};
+ authData = {
+ otpKey: '12345678901234567890123456789012',
+ };
+ });
+ it('covers success', async function () {
+ await manager._otpDisable(dbCtx, ctx, authData);
+ assert(!ctx.otpKey);
+ assert(!authData.otpKey);
+ assert(manager.db.authenticationUpdateOTPKey.called);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.db.authenticationUpdateOTPKey.throws();
+ await manager._otpDisable(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ }); // _otpDisable
+
+ describe('_otpEnsable', function () {
+ it('covers success', async function () {
+ await manager._otpEnable(ctx);
+ assert('otpConfirmKey' in ctx);
+ assert('otpConfirmBox' in ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers failure', async function () {
+ sinon.stub(manager.mysteryBox, 'pack').throws();
+ await manager._otpEnable(ctx);
+ assert(!('otpConfirmKey' in ctx));
+ assert(!('otpConfirmBox' in ctx));
+ assert(ctx.errors.length);
+ });
+ }); // _otpEnsable
+
+ describe('_otpConfirm', function () {
+ let dbCtx, otpState;
+ beforeEach(function () {
+ sinon.stub(Date, 'now').returns(1710435655000);
+ dbCtx = {};
+ ctx.parsedBody = {
+ 'otp-box': 'xxxBoxedStatexxx',
+ 'otp-token': '350876',
+ };
+ otpState = {
+ otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
+ otpAttempt: 0,
+ otpInitiatedMs: 1710434052084,
+ };
+ sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
+ });
+ it('covers success', async function () {
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(manager.db.authenticationUpdateOTPKey.called);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers bad state', async function () {
+ manager.mysteryBox.unpack.throws();
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers no token entered', async function () {
+ ctx.parsedBody['otp-token'] = '';
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(!ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers bad token entered', async function () {
+ ctx.parsedBody['otp-token'] = '123456';
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers db error', async function () {
+ manager.db.authenticationUpdateOTPKey.throws();
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ });
+ }); // _otpConfirm
+
+ describe('_credentialUpdate', function () {
+ let dbCtx, authData;
+ beforeEach(function () {
+ ctx.parsedBody = {
+ 'credential-new': 'abc',
+ 'credential-new-2': 'abc',
+ 'credential-current': '123',
+ };
+ authData = {};
+ manager.authenticator._validateAuthDataCredential.resolves(true);
+ });
+ it('covers success', async function () {
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers invalid current password', async function () {
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers empty new password', async function () {
+ delete ctx.parsedBody['credential-new'];
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers mismatched new password', async function () {
+ ctx.parsedBody['credential-new'] = 'cde';
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.authenticator.updateCredential.throws();
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ assert(manager.logger.error.called);
+ });
+ }); // _credentialUpdate
+
+}); // SessionManager