X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=test%2Flib%2Fsession-manager.js;h=f0aca6d8e339ad1329d125c340ae47dd4425dea6;hb=53ef948ea83106e82d55e60d6695a15e94bf725e;hp=bf8df03b94f8875bfa5306e6f9a59f51546cfbe6;hpb=a44a8c411be1831e7b2418212ee0b295206ca27a;p=squeep-authentication-module diff --git a/test/lib/session-manager.js b/test/lib/session-manager.js index bf8df03..f0aca6d 100644 --- a/test/lib/session-manager.js +++ b/test/lib/session-manager.js @@ -7,27 +7,37 @@ const assert = require('assert'); 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); @@ -37,6 +47,13 @@ describe('SessionManager', function () { 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 () { @@ -45,28 +62,50 @@ describe('SessionManager', 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'; @@ -113,6 +152,21 @@ describe('SessionManager', function () { await manager.postAdminLogin(res, ctx); assert(!res.setHeader.called); }); + it('covers profile scheme fallback', async function () { + ctx.parsedBody.me = 'https://example.com/profile'; + 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', + }, + }); + await manager.postAdminLogin(res, ctx); + assert.strictEqual(res.statusCode, 302); + + }); describe('living-standard-20220212', function () { it('covers valid profile', async function () { ctx.parsedBody.me = 'https://example.com/profile'; @@ -150,6 +204,197 @@ describe('SessionManager', function () { }); // 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); @@ -162,7 +407,9 @@ describe('SessionManager', function () { 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, }); @@ -327,4 +574,205 @@ describe('SessionManager', function () { }); // 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