Skip to content

Commit 82bcfbe

Browse files
committed
feat: Authentication v3 Express integration (#1218)
1 parent 0fa5f7c commit 82bcfbe

File tree

6 files changed

+265
-7
lines changed

6 files changed

+265
-7
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { flatten, merge } = require('lodash');
2+
const { BadRequest } = require('@feathersjs/errors');
3+
4+
const normalizeStrategy = (_settings = [], ..._strategies) =>
5+
typeof _settings === 'string'
6+
? { strategies: flatten([ _settings, ..._strategies ]) }
7+
: _settings;
8+
const getService = (settings, app) => {
9+
const path = settings.service || app.get('defaultAuthentication');
10+
const service = app.service(path);
11+
12+
if (!service) {
13+
throw new BadRequest(`Could not find authentication service '${path}'`);
14+
}
15+
16+
return service;
17+
};
18+
19+
exports.parseAuthentication = (...strategies) => {
20+
const settings = normalizeStrategy(...strategies);
21+
22+
if (!Array.isArray(settings.strategies) || settings.strategies.length === 0) {
23+
throw new Error(`'parseAuthentication' middleware requires at least one strategy name`);
24+
}
25+
26+
return function (req, res, next) {
27+
const { app } = req;
28+
const service = getService(settings, app);
29+
30+
service.parse(req, res, ...settings.strategies)
31+
.then(authentication => {
32+
merge(req, {
33+
authentication,
34+
feathers: { authentication }
35+
});
36+
37+
next();
38+
}).catch(next);
39+
};
40+
};
41+
42+
exports.authenticate = (...strategies) => {
43+
const settings = normalizeStrategy(...strategies);
44+
45+
if (!Array.isArray(settings.strategies) || settings.strategies.length === 0) {
46+
throw new Error(`'authenticate' middleware requires at least one strategy name`);
47+
}
48+
49+
return function (req, res, next) {
50+
const { app, authentication } = req;
51+
const service = getService(settings, app);
52+
53+
service.authenticate(authentication, req.feathers, ...settings.strategies)
54+
.then(authResult => {
55+
merge(req, authResult);
56+
57+
next();
58+
}).catch(next);
59+
};
60+
};

packages/express/lib/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const errorHandler = require('@feathersjs/errors/handler');
44
const notFound = require('@feathersjs/errors/not-found');
55
const debug = require('debug')('@feathersjs/express');
66

7+
const authentication = require('./authentication');
78
const rest = require('./rest');
89

910
function feathersExpress (feathersApp) {
@@ -83,7 +84,7 @@ function feathersExpress (feathersApp) {
8384

8485
module.exports = feathersExpress;
8586

86-
Object.assign(module.exports, express, {
87+
Object.assign(module.exports, express, authentication, {
8788
default: feathersExpress,
8889
original: express,
8990
rest,

packages/express/lib/rest/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function rest (handler = formatter) {
3030
app.rest = wrappers;
3131

3232
app.use(function (req, res, next) {
33-
req.feathers = { provider: 'rest' };
33+
req.feathers = Object.assign({ provider: 'rest' }, req.feathers);
3434
next();
3535
});
3636

packages/express/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@
4242
"uberproto": "^2.0.4"
4343
},
4444
"devDependencies": {
45+
"@feathersjs/authentication": "^2.1.16",
46+
"@feathersjs/authentication-local": "^1.2.9",
4547
"@feathersjs/feathers": "^3.3.1",
4648
"axios": "^0.18.0",
4749
"chai": "^4.2.0",
50+
"lodash": "^4.17.11",
4851
"mocha": "^5.2.0"
4952
}
5053
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
const assert = require('assert');
2+
const _axios = require('axios');
3+
const feathers = require('@feathersjs/feathers');
4+
const getApp = require('@feathersjs/authentication-local/test/fixture');
5+
const { authenticate } = require('@feathersjs/authentication');
6+
7+
const expressify = require('../lib');
8+
const axios = _axios.create({
9+
baseURL: 'http://localhost:9876/'
10+
});
11+
12+
describe('@feathersjs/express/authentication', () => {
13+
const email = 'expresstest@authentication.com';
14+
const password = 'superexpress';
15+
16+
let app, server, user, authResult;
17+
18+
before(() => {
19+
const expressApp = expressify(feathers())
20+
.use(expressify.json())
21+
.use(expressify.parseAuthentication('jwt'))
22+
.configure(expressify.rest());
23+
24+
app = getApp(expressApp);
25+
server = app.listen(9876);
26+
27+
app.use('/dummy', {
28+
get (id, params) {
29+
return Promise.resolve({ id, params });
30+
}
31+
});
32+
33+
app.use('/protected', expressify.authenticate('jwt'), (req, res) => {
34+
res.json(req.user);
35+
});
36+
37+
app.use(expressify.errorHandler({
38+
logger: false
39+
}));
40+
41+
app.service('dummy').hooks({
42+
before: [ authenticate('jwt') ]
43+
});
44+
45+
return app.service('users').create({ email, password })
46+
.then(result => {
47+
user = result;
48+
49+
return axios.post('/authentication', {
50+
strategy: 'local',
51+
password,
52+
email
53+
});
54+
}).then(res => {
55+
authResult = res.data;
56+
});
57+
});
58+
59+
after(done => server.close(done));
60+
61+
it('middleware needs strategies ', () => {
62+
try {
63+
expressify.parseAuthentication();
64+
assert.fail('Should never get here');
65+
} catch (error) {
66+
assert.strictEqual(error.message,
67+
`'parseAuthentication' middleware requires at least one strategy name`
68+
);
69+
}
70+
71+
try {
72+
expressify.authenticate();
73+
assert.fail('Should never get here');
74+
} catch(error) {
75+
assert.strictEqual(error.message,
76+
`'authenticate' middleware requires at least one strategy name`
77+
);
78+
}
79+
});
80+
81+
describe('service authentication', () => {
82+
it('successful local authentication', () => {
83+
assert.ok(authResult.accessToken);
84+
assert.deepStrictEqual(authResult.authentication, {
85+
strategy: 'local'
86+
});
87+
assert.strictEqual(authResult.user.email, email);
88+
assert.strictEqual(authResult.user.password, undefined);
89+
});
90+
91+
it('local authentication with wrong password fails', () => {
92+
return axios.post('/authentication', {
93+
strategy: 'local',
94+
password: 'wrong',
95+
email
96+
}).then(() => {
97+
assert.fail('Should never get here');
98+
}).catch(error => {
99+
const { data } = error.response;
100+
assert.strictEqual(data.name, 'NotAuthenticated');
101+
assert.strictEqual(data.message, 'Invalid login');
102+
});
103+
});
104+
105+
it('authenticating with JWT works but returns same accessToken', () => {
106+
const { accessToken } = authResult;
107+
108+
return axios.post('/authentication', {
109+
strategy: 'jwt',
110+
accessToken
111+
}).then(res => {
112+
const { data } = res;
113+
114+
assert.strictEqual(data.accessToken, accessToken);
115+
assert.strictEqual(data.authentication.strategy, 'jwt');
116+
assert.strictEqual(data.authentication.payload.sub, user.id.toString());
117+
assert.strictEqual(data.user.email, email);
118+
});
119+
});
120+
121+
it('can make a protected request with Authorization header', () => {
122+
const { accessToken } = authResult;
123+
124+
return axios.get('/dummy/dave', {
125+
headers: {
126+
Authorization: accessToken
127+
}
128+
}).then(res => {
129+
const { data, data: { params } } = res;
130+
131+
assert.strictEqual(data.id, 'dave');
132+
assert.deepStrictEqual(params.user, user);
133+
assert.strictEqual(params.authentication.accessToken, accessToken);
134+
});
135+
});
136+
137+
it('can make a protected request with Authorization header and bearer scheme', () => {
138+
const { accessToken } = authResult;
139+
140+
return axios.get('/dummy/dave', {
141+
headers: {
142+
Authorization: ` Bearer: ${accessToken}`
143+
}
144+
}).then(res => {
145+
const { data, data: { params } } = res;
146+
147+
assert.strictEqual(data.id, 'dave');
148+
assert.deepStrictEqual(params.user, user);
149+
assert.strictEqual(params.authentication.accessToken, accessToken);
150+
});
151+
});
152+
});
153+
154+
describe('authenticate middleware', () => {
155+
it('protected endpoint fails when JWT is not present', () => {
156+
return axios.get('/protected').then(() => {
157+
assert.fail('Should never get here');
158+
}).catch(error => {
159+
const { data } = error.response;
160+
161+
assert.strictEqual(data.name, 'NotAuthenticated');
162+
assert.strictEqual(data.message, 'No valid authentication strategy available');
163+
});
164+
});
165+
166+
it.skip('protected endpoint fails with invalid Authorization header', () => {
167+
return axios.get('/protected', {
168+
headers: {
169+
Authorization: 'Bearer: something wrong'
170+
}
171+
}).then(() => {
172+
assert.fail('Should never get here');
173+
}).catch(error => {
174+
const { data } = error.response;
175+
176+
assert.strictEqual(data.name, 'NotAuthenticated');
177+
assert.strictEqual(data.message, 'Not authenticated');
178+
});
179+
});
180+
181+
it('can request protected endpoint with JWT present', () => {
182+
return axios.get('/protected', {
183+
headers: {
184+
Authorization: `Bearer ${authResult.accessToken}`
185+
}
186+
}).then(res => {
187+
const { data } = res;
188+
189+
assert.strictEqual(data.email, user.email);
190+
assert.strictEqual(data.id, user.id);
191+
assert.strictEqual(data.password, undefined, 'Passed provider information');
192+
});
193+
});
194+
});
195+
});

packages/express/test/rest/index.test.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const assert = require('assert');
22
const axios = require('axios');
3-
const bodyParser = require('body-parser');
43

54
const feathers = require('@feathersjs/feathers');
65
const { Service } = require('@feathersjs/commons/lib/test/fixture');
@@ -92,7 +91,7 @@ describe('@feathersjs/express/rest provider', () => {
9291
before(function () {
9392
app = expressify(feathers())
9493
.configure(rest(rest.formatter))
95-
.use(bodyParser.json())
94+
.use(expressify.json())
9695
.use('codes', {
9796
get (id, params) {
9897
return Promise.resolve({ id });
@@ -297,7 +296,7 @@ describe('@feathersjs/express/rest provider', () => {
297296
next();
298297
})
299298
.configure(rest(rest.formatter))
300-
.use(bodyParser.json())
299+
.use(expressify.json())
301300
.use('/todo', {
302301
create (data) {
303302
return Promise.resolve(data);
@@ -325,7 +324,7 @@ describe('@feathersjs/express/rest provider', () => {
325324
const app = expressify(feathers());
326325

327326
app.configure(rest())
328-
.use(bodyParser.json())
327+
.use(expressify.json())
329328
.use('/todo', function (req, res, next) {
330329
req.body.before = [ 'before first' ];
331330
next();
@@ -371,7 +370,7 @@ describe('@feathersjs/express/rest provider', () => {
371370
res.status(200).json(res.data);
372371
}];
373372
app.configure(rest())
374-
.use(bodyParser.json())
373+
.use(expressify.json())
375374
.use('/array-middleware', middlewareArray);
376375

377376
const server = app.listen(4776);

0 commit comments

Comments
 (0)