rudimentary support for totp 2fa
[squeep-authentication-module] / test / lib / authenticator.js
index 22fc9d218eb3953f154cef9bf3d2b692332cf4d3..75761689556c9adda18a7773901037c34bb2b71b 100644 (file)
@@ -6,12 +6,9 @@ const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-requi
 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) {
@@ -35,12 +32,9 @@ describe('Authenticator', function () {
 
   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 () {
@@ -117,6 +111,12 @@ describe('Authenticator', function () {
       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,
@@ -165,12 +165,7 @@ describe('Authenticator', 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(identifier, credential), expected);
     });
     it('covers forbidden', async function () {
       identifier = 'root';
@@ -222,16 +217,13 @@ describe('Authenticator', function () {
 
   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
 
@@ -274,6 +266,40 @@ describe('Authenticator', function () {
     });
   }); // isValidCookieAuth
 
+  describe('checkOTP', function () {
+    let state, otp;
+    this.beforeEach(function () {
+      sinon.stub(authenticator.TOTP.prototype, 'validate').returns(true);
+      state = {
+        key: Buffer.from('12345678901234567890'),
+        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('sessionCheck', function () {
     let cookie, req, res, loginPath, required, profilesAllowed;
     beforeEach(function () {
@@ -299,15 +325,22 @@ describe('Authenticator', function () {
       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;
+      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('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';
@@ -431,14 +464,12 @@ describe('Authenticator', function () {
       req.getHeader.returns('Basic Zm9vOmJhcg==');
       sinon.stub(authenticator, 'sessionCheck').resolves(false);
       sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
-      try {
-        await authenticator.apiRequiredLocal(req, res, ctx);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert.strictEqual(e.statusCode, 401);
-        assert(!authenticator.sessionCheck.called);
-        assert(authenticator.isValidAuthorization.called);
-      }
+      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();
@@ -452,15 +483,13 @@ describe('Authenticator', 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) {
-        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);
     });
   }); // apiRequiredLocal