X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=test%2Flib%2Fauthenticator.js;fp=test%2Flib%2Fauthenticator.js;h=cc75652443568a1a320b4b418de665d55693971a;hb=dd173e6b450cbba8100883514610c9fde83d050a;hp=0000000000000000000000000000000000000000;hpb=7442dd9aef45fe74dc7abe4d111c734068ef2f7d;p=squeep-authentication-module diff --git a/test/lib/authenticator.js b/test/lib/authenticator.js new file mode 100644 index 0000000..cc75652 --- /dev/null +++ b/test/lib/authenticator.js @@ -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