IndieAuth login support, allows viewing of topics related to profile
[websub-hub] / test / src / authenticator.js
index 8c174360b1b815790024c5c59640a57583c9edc7..47313d84c875c84eb4ba6b2c12a9f108da6f4876 100644 (file)
@@ -2,37 +2,51 @@
 'use strict';
 
 const assert = require('assert');
-const sinon = require('sinon');
+const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
 const Authenticator = require('../../src/authenticator');
 const stubLogger = require('../stub-logger');
 const stubDb = require('../stub-db');
 const Errors = require('../../src/errors');
 const Enum = require('../../src/enum');
+const Config = require('../../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 = {
-      authenticator: {
-        basicRealm: 'realm',
-        secureAuthOnly: true,
-      },
-    };
+    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');
+    }
+  });
+
   describe('isValidBasic', function () {
     it('succeeds', async function () {
-      sinon.stub(authenticator.db, 'authenticationGet').resolves({
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
         identifier,
         credential,
       });
@@ -42,7 +56,8 @@ describe('Authenticator', function () {
       assert.strictEqual(ctx.authenticationId, identifier);
     });
     it('fails', async function () {
-      sinon.stub(authenticator.db, 'authenticationGet').resolves({
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
         identifier,
         credential,
       });
@@ -52,14 +67,14 @@ describe('Authenticator', function () {
       assert.strictEqual(ctx.authenticationId, undefined);
     });
     it('covers no entry', async function() {
-      sinon.stub(authenticator.db, 'authenticationGet').resolves();
+      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 () {
-      sinon.stub(authenticator.db, 'authenticationGet').resolves({
+      authenticator.db.authenticationGet.resolves({
         identifier,
         credential: '$other$kind_of_credential',
       });
@@ -70,6 +85,95 @@ describe('Authenticator', function () {
     });
   }); // 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('isValidAuthorization', function () {
     it('handles basic', async function () {
       const expected = true;
@@ -101,6 +205,35 @@ describe('Authenticator', function () {
     });
   }); // requestBasic
 
+  describe('isValidCookieAuth', function () {
+    beforeEach(function () {
+      sinon.stub(authenticator.mysteryBox, 'unpack');
+    });
+    it('covers identifier success', async function () {
+      authenticator.mysteryBox.unpack.resolves({
+        authenticatedIdentifier: 'identifier',
+      });
+      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
+      assert.strictEqual(result, true);
+    });
+    it('covers profile success', async function () {
+      authenticator.mysteryBox.unpack.resolves({
+        authenticatedProfile: 'profile',
+      });
+      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
+      assert.strictEqual(result, true);
+    });
+    it('covers missing cookie', async function () {
+      const result = await authenticator.isValidCookieAuth(ctx, 'wrongCookie');
+      assert.strictEqual(result, false);
+    });
+    it('covers bad cookie', async function () {
+      authenticator.mysteryBox.unpack.rejects();
+      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
+      assert.strictEqual(result, false);
+    });
+  }); // isValidCookieAuth
+
   describe('required', function () {
     let req, res;
     beforeEach(function () {
@@ -109,6 +242,7 @@ describe('Authenticator', function () {
         getHeader: sinon.stub(),
       };
       res = {
+        end: sinon.stub(),
         setHeader: sinon.stub(),
       }
     });
@@ -118,6 +252,12 @@ describe('Authenticator', function () {
       const result = await authenticator.required(req, res, ctx);
       assert.strictEqual(result, true);
     });
+    it('covers valid cookie session', async function () {
+      req.getHeader.returns('WSHas=sessionCookie');
+      sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+      const result = await authenticator.required(req, res, ctx);
+      assert.strictEqual(result, true);
+    });
     it('rejects insecure connection', async function () {
       ctx.clientProtocol = 'http';
       try {
@@ -139,5 +279,66 @@ describe('Authenticator', function () {
         assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
       }
     });
+    it('redirects without any auth', async function () {
+      await authenticator.required(req, res, ctx);
+      assert(res.end.called);
+      assert(res.setHeader.called);
+    });
   }); // required
+
+  describe('requiredLocal', function () {
+    let req, res;
+    beforeEach(function () {
+      ctx.clientProtocol = 'https';
+      req = {
+        getHeader: sinon.stub(),
+      };
+      res = {
+        end: sinon.stub(),
+        setHeader: sinon.stub(),
+      }
+    });
+    it('succeeds', async function() {
+      req.getHeader.returns('auth header');
+      sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
+      const result = await authenticator.requiredLocal(req, res, ctx);
+      assert.strictEqual(result, true);
+    });
+    it('covers valid cookie session', async function () {
+      req.getHeader.returns('WSHas=sessionCookie');
+      sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
+      ctx.session = {
+        authenticatedIdentifier: identifier,
+      };
+      const result = await authenticator.requiredLocal(req, res, ctx);
+      assert.strictEqual(result, true);
+    });
+    it('rejects insecure connection', async function () {
+      ctx.clientProtocol = 'http';
+      try {
+        await authenticator.requiredLocal(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof Errors.ResponseError);
+        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
+      }
+    });
+    it('rejects invalid auth', async function () {
+      try {
+        req.getHeader.returns('auth header');
+        sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
+        await authenticator.requiredLocal(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof Errors.ResponseError);
+        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
+      }
+    });
+    it('redirects without any auth', async function () {
+      await authenticator.requiredLocal(req, res, ctx);
+      assert(res.end.called);
+      assert(res.setHeader.called);
+    });
+  }); // requiredLocal
+
 }); // Authenticator