Skip to content

Commit

Permalink
feat: Authentication v3 Express integration (#1218)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored Feb 18, 2019
1 parent 226859c commit 9792139
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 7 deletions.
60 changes: 60 additions & 0 deletions packages/express/lib/authentication.js
Original file line number Diff line number Diff line change
@@ -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);
};
};
3 changes: 2 additions & 1 deletion packages/express/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/express/lib/rest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
3 changes: 3 additions & 0 deletions packages/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
195 changes: 195 additions & 0 deletions packages/express/test/authentication.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
});
9 changes: 4 additions & 5 deletions packages/express/test/rest/index.test.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 9792139

Please sign in to comment.