X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=test%2Flib%2Fauthenticator.js;h=dea4a84427e2d9634b10e401a9084b1bea5370f9;hb=53ef948ea83106e82d55e60d6695a15e94bf725e;hp=9fa220097d09b0112fc1649defb7e67efe0c3fca;hpb=93fbeab317901033612318b672aff060682df8a6;p=squeep-authentication-module diff --git a/test/lib/authenticator.js b/test/lib/authenticator.js index 9fa2200..dea4a84 100644 --- a/test/lib/authenticator.js +++ b/test/lib/authenticator.js @@ -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) { @@ -28,6 +25,7 @@ describe('Authenticator', function () { ctx = {}; password = 'badPassword'; stubDb._reset(); + stubLogger._reset(); }); afterEach(function () { sinon.restore(); @@ -35,12 +33,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 () { @@ -48,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'); @@ -101,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({ @@ -143,73 +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); - try { - await authenticator._isValidPAMIdentifier(identifier, credential); - assert.fail(noExpectedException); - } catch (e) { - assert.deepStrictEqual(e, 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,76 +298,111 @@ 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 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 + describe('checkOTP', function () { + let state, otp; + this.beforeEach(function () { + sinon.stub(authenticator.TOTP.prototype, 'validate').returns(true); + state = { + key: '12345678901234567890123456789012', + 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; + 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', @@ -307,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', @@ -317,13 +421,10 @@ describe('Authenticator', function () { }); 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'; @@ -339,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', @@ -348,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', @@ -361,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', @@ -370,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', @@ -379,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); @@ -388,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); @@ -398,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', @@ -411,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); @@ -447,14 +540,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(); @@ -468,15 +559,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