From 9aa15aa8a26298acc1ae2f57f0822dc1beec2c58 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Thu, 14 Feb 2019 18:16:17 -0800 Subject: [PATCH 1/2] Initial setup for Express authentication and auth integration tests --- packages/express/lib/authentication.js | 11 +++ packages/express/package.json | 1 + packages/express/test/authentication.test.js | 86 ++++++++++++++++++++ packages/express/test/rest/index.test.js | 9 +- 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 packages/express/lib/authentication.js create mode 100644 packages/express/test/authentication.test.js diff --git a/packages/express/lib/authentication.js b/packages/express/lib/authentication.js new file mode 100644 index 0000000000..55c53698e3 --- /dev/null +++ b/packages/express/lib/authentication.js @@ -0,0 +1,11 @@ +exports.parseAuthentication = (...strategies) => { + return function (req, res, next) { + + }; +}; + +exports.authenticate = (...strategies) => { + return function (req, res, next) { + + }; +}; diff --git a/packages/express/package.json b/packages/express/package.json index 4350344676..d820b9ed0d 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -42,6 +42,7 @@ "uberproto": "^2.0.4" }, "devDependencies": { + "@feathersjs/authentication-local": "^1.2.9", "@feathersjs/feathers": "^3.3.1", "axios": "^0.18.0", "chai": "^4.2.0", diff --git a/packages/express/test/authentication.test.js b/packages/express/test/authentication.test.js new file mode 100644 index 0000000000..223c5109f7 --- /dev/null +++ b/packages/express/test/authentication.test.js @@ -0,0 +1,86 @@ +const assert = require('assert'); +const _axios = require('axios'); +const feathers = require('@feathersjs/feathers'); +const getApp = require('@feathersjs/authentication-local/test/fixture'); +const axios = _axios.create({ + baseURL: 'http://localhost:9876/' +}); +const expressify = require('../lib'); + +describe('@feathersjs/express/authentication', () => { + const email = 'expresstest@authentication.com'; + const password = 'superexpress'; + + let app, server, user, authResult; + + before(() => { + const expressApp = expressify(feathers()) + .use(expressify.json()) + .configure(expressify.rest()); + + app = getApp(expressApp); + server = app.listen(9876); + + app.use('/protected', { + get (id, params) { + return Promise.resolve({ id, params }); + } + }); + + app.use(expressify.errorHandler()); + + return app.service('users').create({ email, password }) + .then(result => { + user = result; + + return axios.post('/authentication', { + strategy: 'local', + password, + email + }); + }).then(res => { + authResult = res.data; + }); + }); + + after(done => server.close(done)); + + it('successful local authentication', () => { + assert.ok(authResult.accessToken); + assert.deepStrictEqual(authResult.authentication, { + strategy: 'local' + }); + assert.strictEqual(authResult.user.email, email); + assert.strictEqual(authResult.user.password, undefined); + }); + + it('local authentication with wrong password fails', () => { + return axios.post('/authentication', { + strategy: 'local', + password: 'wrong', + email + }).then(() => { + assert.fail('Should never get here'); + }).catch(error => { + const { data } = error.response; + assert.strictEqual(data.name, 'NotAuthenticated'); + assert.strictEqual(data.message, 'Invalid login'); + }); + }); + + it('authenticating with JWT works but returns same accessToken', () => { + const { accessToken } = authResult; + + return axios.post('/authentication', { + strategy: 'jwt', + accessToken + }).then(res => { + const { data } = res; + + assert.strictEqual(data.accessToken, accessToken); + assert.strictEqual(data.authentication.strategy, 'jwt'); + assert.strictEqual(data.authentication.payload.sub, user.id.toString()); + assert.strictEqual(data.user.email, email); + }); + }); +}); diff --git a/packages/express/test/rest/index.test.js b/packages/express/test/rest/index.test.js index 9fc1821cb1..88516b32e9 100644 --- a/packages/express/test/rest/index.test.js +++ b/packages/express/test/rest/index.test.js @@ -1,6 +1,5 @@ const assert = require('assert'); const axios = require('axios'); -const bodyParser = require('body-parser'); const feathers = require('@feathersjs/feathers'); const { Service } = require('@feathersjs/commons/lib/test/fixture'); @@ -92,7 +91,7 @@ describe('@feathersjs/express/rest provider', () => { before(function () { app = expressify(feathers()) .configure(rest(rest.formatter)) - .use(bodyParser.json()) + .use(expressify.json()) .use('codes', { get (id, params) { return Promise.resolve({ id }); @@ -297,7 +296,7 @@ describe('@feathersjs/express/rest provider', () => { next(); }) .configure(rest(rest.formatter)) - .use(bodyParser.json()) + .use(expressify.json()) .use('/todo', { create (data) { return Promise.resolve(data); @@ -325,7 +324,7 @@ describe('@feathersjs/express/rest provider', () => { const app = expressify(feathers()); app.configure(rest()) - .use(bodyParser.json()) + .use(expressify.json()) .use('/todo', function (req, res, next) { req.body.before = [ 'before first' ]; next(); @@ -371,7 +370,7 @@ describe('@feathersjs/express/rest provider', () => { res.status(200).json(res.data); }]; app.configure(rest()) - .use(bodyParser.json()) + .use(expressify.json()) .use('/array-middleware', middlewareArray); const server = app.listen(4776); From dfeacd70b020989d94b00acd98a24309d2c48016 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 17 Feb 2019 23:10:33 -0800 Subject: [PATCH 2/2] Finalize authentication middleware and Express authentication integration tests --- packages/express/lib/authentication.js | 51 +++++- packages/express/lib/index.js | 3 +- packages/express/lib/rest/index.js | 2 +- packages/express/package.json | 2 + packages/express/test/authentication.test.js | 175 +++++++++++++++---- 5 files changed, 197 insertions(+), 36 deletions(-) diff --git a/packages/express/lib/authentication.js b/packages/express/lib/authentication.js index 55c53698e3..01775382cf 100644 --- a/packages/express/lib/authentication.js +++ b/packages/express/lib/authentication.js @@ -1,11 +1,60 @@ +const { flatten, merge } = require('lodash'); +const { BadRequest } = require('@feathersjs/errors'); + +const normalizeStrategy = (_settings = [], ..._strategies) => + typeof _settings === 'string' + ? { strategies: flatten([ _settings, ..._strategies ]) } + : _settings; +const getService = (settings, app) => { + const path = settings.service || app.get('defaultAuthentication'); + const service = app.service(path); + + if (!service) { + throw new BadRequest(`Could not find authentication service '${path}'`); + } + + return service; +}; + exports.parseAuthentication = (...strategies) => { + const settings = normalizeStrategy(...strategies); + + if (!Array.isArray(settings.strategies) || settings.strategies.length === 0) { + throw new Error(`'parseAuthentication' middleware requires at least one strategy name`); + } + return function (req, res, next) { - + const { app } = req; + const service = getService(settings, app); + + service.parse(req, res, ...settings.strategies) + .then(authentication => { + merge(req, { + authentication, + feathers: { authentication } + }); + + next(); + }).catch(next); }; }; exports.authenticate = (...strategies) => { + const settings = normalizeStrategy(...strategies); + + if (!Array.isArray(settings.strategies) || settings.strategies.length === 0) { + throw new Error(`'authenticate' middleware requires at least one strategy name`); + } + return function (req, res, next) { + const { app, authentication } = req; + const service = getService(settings, app); + + service.authenticate(authentication, req.feathers, ...settings.strategies) + .then(authResult => { + merge(req, authResult); + next(); + }).catch(next); }; }; diff --git a/packages/express/lib/index.js b/packages/express/lib/index.js index 794479ffd3..18e3dcabdc 100644 --- a/packages/express/lib/index.js +++ b/packages/express/lib/index.js @@ -4,6 +4,7 @@ const errorHandler = require('@feathersjs/errors/handler'); const notFound = require('@feathersjs/errors/not-found'); const debug = require('debug')('@feathersjs/express'); +const authentication = require('./authentication'); const rest = require('./rest'); function feathersExpress (feathersApp) { @@ -83,7 +84,7 @@ function feathersExpress (feathersApp) { module.exports = feathersExpress; -Object.assign(module.exports, express, { +Object.assign(module.exports, express, authentication, { default: feathersExpress, original: express, rest, diff --git a/packages/express/lib/rest/index.js b/packages/express/lib/rest/index.js index 29b135e4df..e10c2b537a 100644 --- a/packages/express/lib/rest/index.js +++ b/packages/express/lib/rest/index.js @@ -30,7 +30,7 @@ function rest (handler = formatter) { app.rest = wrappers; app.use(function (req, res, next) { - req.feathers = { provider: 'rest' }; + req.feathers = Object.assign({ provider: 'rest' }, req.feathers); next(); }); diff --git a/packages/express/package.json b/packages/express/package.json index d820b9ed0d..123f15ddf5 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -42,10 +42,12 @@ "uberproto": "^2.0.4" }, "devDependencies": { + "@feathersjs/authentication": "^2.1.16", "@feathersjs/authentication-local": "^1.2.9", "@feathersjs/feathers": "^3.3.1", "axios": "^0.18.0", "chai": "^4.2.0", + "lodash": "^4.17.11", "mocha": "^5.2.0" } } diff --git a/packages/express/test/authentication.test.js b/packages/express/test/authentication.test.js index 223c5109f7..94a8bcb1a8 100644 --- a/packages/express/test/authentication.test.js +++ b/packages/express/test/authentication.test.js @@ -2,10 +2,12 @@ const assert = require('assert'); const _axios = require('axios'); const feathers = require('@feathersjs/feathers'); const getApp = require('@feathersjs/authentication-local/test/fixture'); +const { authenticate } = require('@feathersjs/authentication'); + +const expressify = require('../lib'); const axios = _axios.create({ baseURL: 'http://localhost:9876/' }); -const expressify = require('../lib'); describe('@feathersjs/express/authentication', () => { const email = 'expresstest@authentication.com'; @@ -16,18 +18,29 @@ describe('@feathersjs/express/authentication', () => { before(() => { const expressApp = expressify(feathers()) .use(expressify.json()) + .use(expressify.parseAuthentication('jwt')) .configure(expressify.rest()); app = getApp(expressApp); server = app.listen(9876); - app.use('/protected', { + app.use('/dummy', { get (id, params) { return Promise.resolve({ id, params }); } }); - - app.use(expressify.errorHandler()); + + app.use('/protected', expressify.authenticate('jwt'), (req, res) => { + res.json(req.user); + }); + + app.use(expressify.errorHandler({ + logger: false + })); + + app.service('dummy').hooks({ + before: [ authenticate('jwt') ] + }); return app.service('users').create({ email, password }) .then(result => { @@ -45,42 +58,138 @@ describe('@feathersjs/express/authentication', () => { after(done => server.close(done)); - it('successful local authentication', () => { - assert.ok(authResult.accessToken); - assert.deepStrictEqual(authResult.authentication, { - strategy: 'local' - }); - assert.strictEqual(authResult.user.email, email); - assert.strictEqual(authResult.user.password, undefined); - }); + it('middleware needs strategies ', () => { + try { + expressify.parseAuthentication(); + assert.fail('Should never get here'); + } catch (error) { + assert.strictEqual(error.message, + `'parseAuthentication' middleware requires at least one strategy name` + ); + } - it('local authentication with wrong password fails', () => { - return axios.post('/authentication', { - strategy: 'local', - password: 'wrong', - email - }).then(() => { + try { + expressify.authenticate(); assert.fail('Should never get here'); - }).catch(error => { - const { data } = error.response; - assert.strictEqual(data.name, 'NotAuthenticated'); - assert.strictEqual(data.message, 'Invalid login'); + } catch(error) { + assert.strictEqual(error.message, + `'authenticate' middleware requires at least one strategy name` + ); + } + }); + + describe('service authentication', () => { + it('successful local authentication', () => { + assert.ok(authResult.accessToken); + assert.deepStrictEqual(authResult.authentication, { + strategy: 'local' + }); + assert.strictEqual(authResult.user.email, email); + assert.strictEqual(authResult.user.password, undefined); + }); + + it('local authentication with wrong password fails', () => { + return axios.post('/authentication', { + strategy: 'local', + password: 'wrong', + email + }).then(() => { + assert.fail('Should never get here'); + }).catch(error => { + const { data } = error.response; + assert.strictEqual(data.name, 'NotAuthenticated'); + assert.strictEqual(data.message, 'Invalid login'); + }); + }); + + it('authenticating with JWT works but returns same accessToken', () => { + const { accessToken } = authResult; + + return axios.post('/authentication', { + strategy: 'jwt', + accessToken + }).then(res => { + const { data } = res; + + assert.strictEqual(data.accessToken, accessToken); + assert.strictEqual(data.authentication.strategy, 'jwt'); + assert.strictEqual(data.authentication.payload.sub, user.id.toString()); + assert.strictEqual(data.user.email, email); + }); + }); + + it('can make a protected request with Authorization header', () => { + const { accessToken } = authResult; + + return axios.get('/dummy/dave', { + headers: { + Authorization: accessToken + } + }).then(res => { + const { data, data: { params } } = res; + + assert.strictEqual(data.id, 'dave'); + assert.deepStrictEqual(params.user, user); + assert.strictEqual(params.authentication.accessToken, accessToken); + }); + }); + + it('can make a protected request with Authorization header and bearer scheme', () => { + const { accessToken } = authResult; + + return axios.get('/dummy/dave', { + headers: { + Authorization: ` Bearer: ${accessToken}` + } + }).then(res => { + const { data, data: { params } } = res; + + assert.strictEqual(data.id, 'dave'); + assert.deepStrictEqual(params.user, user); + assert.strictEqual(params.authentication.accessToken, accessToken); + }); }); }); + + describe('authenticate middleware', () => { + it('protected endpoint fails when JWT is not present', () => { + return axios.get('/protected').then(() => { + assert.fail('Should never get here'); + }).catch(error => { + const { data } = error.response; - it('authenticating with JWT works but returns same accessToken', () => { - const { accessToken } = authResult; + assert.strictEqual(data.name, 'NotAuthenticated'); + assert.strictEqual(data.message, 'No valid authentication strategy available'); + }); + }); + + it.skip('protected endpoint fails with invalid Authorization header', () => { + return axios.get('/protected', { + headers: { + Authorization: 'Bearer: something wrong' + } + }).then(() => { + assert.fail('Should never get here'); + }).catch(error => { + const { data } = error.response; + + assert.strictEqual(data.name, 'NotAuthenticated'); + assert.strictEqual(data.message, 'Not authenticated'); + }); + }); - return axios.post('/authentication', { - strategy: 'jwt', - accessToken - }).then(res => { - const { data } = res; + it('can request protected endpoint with JWT present', () => { + return axios.get('/protected', { + headers: { + Authorization: `Bearer ${authResult.accessToken}` + } + }).then(res => { + const { data } = res; - assert.strictEqual(data.accessToken, accessToken); - assert.strictEqual(data.authentication.strategy, 'jwt'); - assert.strictEqual(data.authentication.payload.sub, user.id.toString()); - assert.strictEqual(data.user.email, email); + assert.strictEqual(data.email, user.email); + assert.strictEqual(data.id, user.id); + assert.strictEqual(data.password, undefined, 'Passed provider information'); + }); }); }); });