rudimentary support for totp 2fa
[squeep-authentication-module] / test / lib / session-manager.js
index a3efeafce3b812fcfd3961393e4e015a5986afad..79abbf007e8805ab05018aa77a5aae541489c62c 100644 (file)
@@ -7,6 +7,7 @@ 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');
 
@@ -25,9 +26,11 @@ describe('SessionManager', function () {
       params: {},
       queryParams: {},
       parsedBody: {},
+      errors: [],
     };
     stubAuthenticator = {
       isValidIdentifierCredential: sinon.stub(),
+      checkOTP: sinon.stub(),
     };
     manager = new SessionManager(stubLogger, stubAuthenticator, options);
     sinon.stub(manager.indieAuthCommunication);
@@ -61,12 +64,27 @@ describe('SessionManager', function () {
   }); // _sessionCookieSet
 
   describe('getAdminLogin', function () {
-    it('covers', async 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';
@@ -115,15 +133,15 @@ describe('SessionManager', function () {
     });
     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);
 
@@ -165,6 +183,103 @@ describe('SessionManager', function () {
     }); // living-standard-20220212
   }); // postAdminLogin
 
+  describe('_otpSubmission', function () {
+    beforeEach(function () {
+      sinon.useFakeTimers(new Date());
+      sinon.stub(manager.mysteryBox, 'unpack').resolves({
+        authenticatedIdentifier: 'identifier',
+        attempt: 0,
+        epochMs: Date.now(),
+      });
+      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 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 () {
+      manager.mysteryBox.unpack.resolves({
+        authenticatedIdentifier: 'identifier',
+        attempt: 10,
+        epochMs: Date.now(),
+      });
+      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 () {
+      manager.mysteryBox.unpack.resolves({
+        authenticatedIdentifier: 'identifier',
+        attempt: 0,
+        epochMs: Date.now() - 99999999,
+      });
+      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 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('_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.otpNeeded = true;
+      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);