initial commit
[squeep-authentication-module] / test / lib / authenticator.js
diff --git a/test/lib/authenticator.js b/test/lib/authenticator.js
new file mode 100644 (file)
index 0000000..cc75652
--- /dev/null
@@ -0,0 +1,409 @@
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
+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) {
+    if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
+      this.skip();
+    }
+  };
+
+  beforeEach(function () {
+    options = Config('test');
+    authenticator = new Authenticator(stubLogger, stubDb, options);
+    identifier = 'username';
+    credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
+    ctx = {};
+    password = 'badPassword';
+    stubDb._reset();
+  });
+  afterEach(function () {
+    sinon.restore();
+  });
+
+  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');
+    }
+  });
+
+  it('covers empty realm', function () {
+    options.authenticator.basicRealm = undefined;
+    authenticator = new Authenticator(stubLogger, stubDb, options);
+  });
+
+  describe('isValidBasic', function () {
+    it('succeeds', async function () {
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential,
+      });
+      const authString = `${identifier}:${password}`;
+      const result = await authenticator.isValidBasic(authString, ctx);
+      assert.strictEqual(result, true);
+      assert.strictEqual(ctx.authenticationId, identifier);
+    });
+    it('fails', async function () {
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential,
+      });
+      const authString = `${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 result = await authenticator.isValidBasic(authString, ctx);
+      assert.strictEqual(result, false);
+      assert.strictEqual(ctx.authenticationId, undefined);
+    });
+    it('covers unknown password hash', async function () {
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential: '$other$kind_of_credential',
+      });
+      const authString = `${identifier}:wrongPassword}`;
+      const result = await authenticator.isValidBasic(authString, ctx);
+      assert.strictEqual(result, false);
+      assert.strictEqual(ctx.authenticationId, undefined);
+    });
+  }); // isValidBasic
+
+  describe('isValidIdentifierCredential', function () {
+    it('succeeds', async function () {
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential,
+      });
+      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+      assert.strictEqual(result, true);
+      assert.strictEqual(ctx.authenticationId, identifier);
+    });
+    it('fails', async function () {
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential,
+      });
+      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+      assert.strictEqual(result, false);
+      assert.strictEqual(ctx.authenticationId, undefined);
+    });
+    it('covers no entry', async function() {
+      authenticator.db.authenticationGet.resolves();
+      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+      assert.strictEqual(result, false);
+      assert.strictEqual(ctx.authenticationId, undefined);
+    });
+    it('covers unknown password hash', async function () {
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential: '$other$kind_of_credential',
+      });
+      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
+      assert.strictEqual(result, false);
+      assert.strictEqual(ctx.authenticationId, undefined);
+    });
+    it('covers PAM', async function () {
+      _authMechanismRequired(authenticator, 'pam');
+      sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential: '$PAM$',
+      });
+      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+      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 () {
+    beforeEach(function () {
+      _authMechanismRequired(authenticator, 'pam');
+      sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
+    });
+    it('covers success', async function () {
+      authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
+      const result = await authenticator._isValidPAMIdentifier(identifier, 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);
+      assert.strictEqual(result, false);
+    });
+    it('covers error', async 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);
+      }
+    });
+    it('covers forbidden', async function () {
+      identifier = 'root';
+      const result = await authenticator._isValidPAMIdentifier(identifier, 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;
+      const authorizationHeader = 'basic Zm9vOmJhcg==';
+      sinon.stub(authenticator, 'isValidBasic').resolves(expected);
+      const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
+      assert.strictEqual(result, expected);
+    });
+    it('handles other', async function () {
+      const expected = false;
+      const authorizationHeader = 'bearer Zm9vOmJhcg==';
+      const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
+      assert.strictEqual(result, expected);
+    });
+  }); // isValidAuthorization
+
+  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);
+      }
+    });
+  }); // requestBasic
+
+  describe('isValidCookieAuth', function () {
+    let cookie;
+    beforeEach(function () {
+      sinon.stub(authenticator.mysteryBox, 'unpack');
+      cookie = 'squeepSession=dummy';
+    });
+    it('covers identifier success', async function () {
+      authenticator.mysteryBox.unpack.resolves({
+        authenticatedIdentifier: 'identifier',
+      });
+      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      assert.strictEqual(result, true);
+    });
+    it('covers profile success', async function () {
+      authenticator.mysteryBox.unpack.resolves({
+        authenticatedProfile: 'profile',
+      });
+      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      assert.strictEqual(result, true);
+    });
+    it('covers missing cookie', async function () {
+      cookie = 'wrong=cookie';
+      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      assert.strictEqual(result, false);
+    });
+    it('covers bad cookie', async function () {
+      authenticator.mysteryBox.unpack.rejects();
+      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      assert.strictEqual(result, false);
+    });
+    it('covers broken session', async function () {
+      authenticator.mysteryBox.unpack.resolves({
+        randomData: 'foo',
+      });
+      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      assert.strictEqual(result, false);
+    });
+  }); // isValidCookieAuth
+
+  describe('sessionCheck', function () {
+    let cookie, req, res, loginPath, required, profilesAllowed;
+    beforeEach(function () {
+      cookie = 'squeepSession=sessionCookie';
+      ctx.clientProtocol = 'https';
+      req = {
+        getHeader: sinon.stub(),
+      };
+      res = {
+        end: sinon.stub(),
+        setHeader: 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',
+      };
+      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);
+      }
+    });
+    it('ignores insecure connection if auth not required', async function () {
+      ctx.clientProtocol = 'http';
+      required = false;
+      const result = await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+      assert.strictEqual(result, false);
+    });
+    it('redirects without any auth', async function () {
+      await authenticator.sessionCheck(req, res, ctx, loginPath, required, profilesAllowed);
+      assert(res.end.called);
+      assert(res.setHeader.called);
+    });
+    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',
+          };
+          const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
+          assert.strictEqual(result, true);
+        });
+        it('redirects with profile', async function () {
+          req.getHeader.returns(cookie);
+          sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+          ctx.session = {
+            authenticatedProfile: 'user',
+          };
+          const result = await authenticator.sessionRequiredLocal(req, res, ctx, loginPath);
+          assert.strictEqual(result, false);
+          assert(res.end.called);
+          assert(res.setHeader.called);
+        });
+      }); // sessionRequiredLocal
+      describe('sessionRequired', function () {
+        it('accepts identifier', async function () {
+          req.getHeader.returns(cookie);
+          sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+          ctx.session = {
+            authenticatedIdentifier: 'user',
+          };
+          const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+          assert.strictEqual(result, true);
+        });
+        it('accepts profile', async function () {
+          req.getHeader.returns(cookie);
+          sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+          ctx.session = {
+            authenticatedProfile: 'user',
+          };
+          const result = await authenticator.sessionRequired(req, res, ctx, loginPath);
+          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);
+          assert(res.end.called);
+          assert(res.setHeader.called);
+        });
+        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);
+          assert(res.end.called);
+          assert(res.setHeader.called);
+        });
+      }); // sessionRequired
+      describe('sessionOptionalLocal', function () {
+        it('rejects profile', async function () {
+          req.getHeader.returns(cookie);
+          sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+          ctx.session = {
+            authenticatedProfile: 'user',
+          };
+          const result = await authenticator.sessionOptionalLocal(req, res, ctx, loginPath);
+          assert.strictEqual(result, false);
+          assert(!res.end.called);
+          assert(!res.setHeader.called);
+        });
+      }); // 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);
+          assert(!res.end.called);
+          assert(!res.setHeader.called);
+        });
+      }); // sessionOptional
+    }); // convenience wrappers
+  }); // sessionCheck
+
+}); // Authenticator