add account settings page, rest of otp support, stdio credential helper, other misc
[squeep-authentication-module] / test / lib / authenticator.js
index 75761689556c9adda18a7773901037c34bb2b71b..dea4a84427e2d9634b10e401a9084b1bea5370f9 100644 (file)
@@ -25,6 +25,7 @@ describe('Authenticator', function () {
     ctx = {};
     password = 'badPassword';
     stubDb._reset();
+    stubLogger._reset();
   });
   afterEach(function () {
     sinon.restore();
@@ -42,6 +43,100 @@ describe('Authenticator', function () {
     authenticator = new Authenticator(stubLogger, stubDb, options);
   });
 
+  it('covers option defaults', function () {
+    delete options.authenticator.secureAuthOnly;
+    delete options.dingus?.proxyPrefix;
+    delete options.authenticator.forbiddenPAMIdentifiers;
+    options.authenticator.authnEnabled.push('flarpyauth');
+    authenticator = new Authenticator(stubLogger, stubDb, options);
+  });
+
+  describe('createIdentifier', function () {
+    let dbCtx;
+    beforeEach(function () {
+      dbCtx = {};
+      credential = 'badpassword';
+    });
+    it('covers success', async function () {
+      const otpKey = '1234567890123456789012';
+      await authenticator.createIdentifier(dbCtx, identifier, credential, otpKey);
+      assert(authenticator.db.authenticationInsertIdentifier.called);
+    });
+    it('covers failure', async function () {
+      const expected = new Error('blah');
+      await authenticator.db.authenticationInsertIdentifier.rejects(expected);
+      // assert.rejects was not happy to handle this for some reason
+      try {
+        await authenticator.createIdentifier(dbCtx, identifier, credential);
+        assert.fail('no expecte exception');
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+        assert(authenticator.db.authenticationInsertIdentifier.called);
+        assert(authenticator.logger.error.called);
+        }
+    });
+  }); // createIdentifier
+
+  describe('updateCredential', function () {
+    let dbCtx, newCredential;
+    beforeEach(function () {
+      dbCtx = {};
+      newCredential = 'newpassword';
+    });
+    it('covers success', async function () {
+      await authenticator.updateCredential(dbCtx, identifier, newCredential);
+      assert(authenticator.db.authenticationUpdateCredential.called);
+      assert(authenticator.logger.info.called);
+    });
+    it('covers failure', async function () {
+      const expected = new Error('foo');
+      authenticator.db.authenticationUpdateCredential.rejects(expected);
+      try {
+        await authenticator.updateCredential(dbCtx, identifier, newCredential);
+        assert.fail('no expected exception');
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+        assert(authenticator.logger.error.called);
+      }
+      // assert.rejects was not happy to handle this for some reason
+    });
+  }); // updateCredential
+
+  describe('_secureCredential', function () {
+    beforeEach(function () {
+      credential = 'badpassword';
+    });
+    it('covers plain', async function () {
+      const result = await authenticator._secureCredential(credential, 'plain');
+      assert.strictEqual(result, '$plain$' + credential);
+    });
+    it('covers default (argon2)', async function () {
+      const result = await authenticator._secureCredential(credential);
+      assert(result.startsWith('$argon2'));
+    });
+    it('covers invalid authn', async function () {
+      const authn = 'bogus';
+      assert.rejects(async () => await authenticator._secureCredential(credential, authn), RangeError);
+    });
+  }); // _secureCredential
+
+  describe('_validateAuthDataCredential', function () {
+    let authData;
+    beforeEach(function () {
+      credential = 'badpassword';
+      authData = {};
+    });
+    it('fails if not provided a credential', async function () {
+      const result = await authenticator._validateAuthDataCredential(authData, credential);
+      assert.strictEqual(result, false);
+    });
+    it('covers plain', async function () {
+      authData.credential = '$plain$badpassword';
+      const result = await authenticator._validateAuthDataCredential(authData, credential);
+      assert.strictEqual(result, true);
+    });
+  }); // _validateAuthDataCredential
+
   describe('isValidBasic', function () {
     it('succeeds', async function () {
       _authMechanismRequired(authenticator, 'argon2');
@@ -95,6 +190,19 @@ describe('Authenticator', function () {
       assert.strictEqual(result, true);
       assert.strictEqual(ctx.authenticationId, identifier);
     });
+    it('succeeds with OTP', async function () {
+      const otpKey = Buffer.from('1234567890');
+      _authMechanismRequired(authenticator, 'argon2');
+      authenticator.db.authenticationGet.resolves({
+        identifier,
+        credential,
+        otpKey,
+      });
+      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
+      assert.strictEqual(result, true);
+      assert.strictEqual(ctx.authenticationId, identifier);
+      assert.deepStrictEqual(ctx.otpKey, otpKey);
+    });
     it('fails', async function () {
       _authMechanismRequired(authenticator, 'argon2');
       authenticator.db.authenticationGet.resolves({
@@ -137,68 +245,41 @@ describe('Authenticator', function () {
       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 () {
+    let authData;
     beforeEach(function () {
       _authMechanismRequired(authenticator, 'pam');
       sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
+      authData = {
+        identifier,
+      };
     });
     it('covers success', async function () {
       authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
-      const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+      const result = await authenticator._isValidPAMIdentifier(authData, 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);
+      const result = await authenticator._isValidPAMIdentifier(authData, credential);
       assert.strictEqual(result, false);
     });
     it('covers error', async function () {
       _authMechanismRequired(authenticator, 'pam');
       const expected = new Error('blah');
       authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
-      assert.rejects(() => authenticator._isValidPAMIdentifier(identifier, credential), expected);
+      assert.rejects(() => authenticator._isValidPAMIdentifier(authData, credential), expected);
     });
     it('covers forbidden', async function () {
-      identifier = 'root';
-      const result = await authenticator._isValidPAMIdentifier(identifier, credential);
+      authData.identifier = 'root';
+      const result = await authenticator._isValidPAMIdentifier(authData, 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;
@@ -228,40 +309,42 @@ describe('Authenticator', function () {
   }); // requestBasic
 
   describe('isValidCookieAuth', function () {
-    let cookie;
     beforeEach(function () {
       sinon.stub(authenticator.mysteryBox, 'unpack');
-      cookie = 'squeepSession=dummy';
+      ctx.cookie = {
+        squeepSession: 'dummy',
+        otherCookie: 'foo',
+      };
     });
     it('covers identifier success', async function () {
       authenticator.mysteryBox.unpack.resolves({
         authenticatedIdentifier: 'identifier',
       });
-      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      const result = await authenticator.isValidCookieAuth(ctx);
       assert.strictEqual(result, true);
     });
     it('covers profile success', async function () {
       authenticator.mysteryBox.unpack.resolves({
         authenticatedProfile: 'profile',
       });
-      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      const result = await authenticator.isValidCookieAuth(ctx);
       assert.strictEqual(result, true);
     });
     it('covers missing cookie', async function () {
-      cookie = 'wrong=cookie';
-      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      delete ctx.cookie.squeepSession;
+      const result = await authenticator.isValidCookieAuth(ctx);
       assert.strictEqual(result, false);
     });
     it('covers bad cookie', async function () {
       authenticator.mysteryBox.unpack.rejects();
-      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      const result = await authenticator.isValidCookieAuth(ctx);
       assert.strictEqual(result, false);
     });
     it('covers broken session', async function () {
       authenticator.mysteryBox.unpack.resolves({
         randomData: 'foo',
       });
-      const result = await authenticator.isValidCookieAuth(ctx, cookie);
+      const result = await authenticator.isValidCookieAuth(ctx);
       assert.strictEqual(result, false);
     });
   }); // isValidCookieAuth
@@ -271,7 +354,7 @@ describe('Authenticator', function () {
     this.beforeEach(function () {
       sinon.stub(authenticator.TOTP.prototype, 'validate').returns(true);
       state = {
-        key: Buffer.from('12345678901234567890'),
+        key: '12345678901234567890123456789012',
         attempt: 0,
         epochMs: Date.now(),
       };
@@ -301,23 +384,25 @@ describe('Authenticator', function () {
   }); // checkOTP
 
   describe('sessionCheck', function () {
-    let cookie, req, res, loginPath, required, profilesAllowed;
+    let req, res, loginPath, required, profilesAllowed;
     beforeEach(function () {
-      cookie = 'squeepSession=sessionCookie';
       ctx.clientProtocol = 'https';
+      ctx.cookie = {
+        squeepSession: 'squeep_session_blob',
+      };
       req = {
         getHeader: sinon.stub(),
       };
       res = {
         end: sinon.stub(),
         setHeader: sinon.stub(),
+        appendHeader: 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',
@@ -327,7 +412,6 @@ describe('Authenticator', function () {
     });
     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',
@@ -356,7 +440,6 @@ describe('Authenticator', function () {
     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',
@@ -365,7 +448,6 @@ describe('Authenticator', function () {
           assert.strictEqual(result, true);
         });
         it('redirects with profile', async function () {
-          req.getHeader.returns(cookie);
           sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
           ctx.session = {
             authenticatedProfile: 'user',
@@ -378,7 +460,6 @@ describe('Authenticator', function () {
       }); // sessionRequiredLocal
       describe('sessionRequired', function () {
         it('accepts identifier', async function () {
-          req.getHeader.returns(cookie);
           sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
           ctx.session = {
             authenticatedIdentifier: 'user',
@@ -387,7 +468,6 @@ describe('Authenticator', function () {
           assert.strictEqual(result, true);
         });
         it('accepts profile', async function () {
-          req.getHeader.returns(cookie);
           sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
           ctx.session = {
             authenticatedProfile: 'user',
@@ -396,7 +476,6 @@ describe('Authenticator', function () {
           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);
@@ -405,7 +484,6 @@ describe('Authenticator', function () {
         });
         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);
@@ -415,7 +493,6 @@ describe('Authenticator', function () {
       }); // sessionRequired
       describe('sessionOptionalLocal', function () {
         it('rejects profile', async function () {
-          req.getHeader.returns(cookie);
           sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
           ctx.session = {
             authenticatedProfile: 'user',
@@ -428,7 +505,6 @@ describe('Authenticator', function () {
       }); // 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);