diff --git a/packages/express/lib/authentication.js b/packages/express/lib/authentication.js new file mode 100644 index 0000000000..01775382cf --- /dev/null +++ b/packages/express/lib/authentication.js @@ -0,0 +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 4350344676..123f15ddf5 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -42,9 +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 new file mode 100644 index 0000000000..94a8bcb1a8 --- /dev/null +++ b/packages/express/test/authentication.test.js @@ -0,0 +1,195 @@ +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/' +}); + +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()) + .use(expressify.parseAuthentication('jwt')) + .configure(expressify.rest()); + + app = getApp(expressApp); + server = app.listen(9876); + + app.use('/dummy', { + get (id, params) { + return Promise.resolve({ id, params }); + } + }); + + 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 => { + user = result; + + return axios.post('/authentication', { + strategy: 'local', + password, + email + }); + }).then(res => { + authResult = res.data; + }); + }); + + after(done => server.close(done)); + + 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` + ); + } + + try { + expressify.authenticate(); + assert.fail('Should never get here'); + } 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; + + 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'); + }); + }); + + 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.email, user.email); + assert.strictEqual(data.id, user.id); + assert.strictEqual(data.password, undefined, 'Passed provider information'); + }); + }); + }); +}); 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);