Skip to content

Commit 6f44a85

Browse files
authored
Release/v3 (#33)
* Don't overwrite Error object in tests Instead of overwriting the default Error object with Foundation's custom error object, name that one FoundationError. * Fix assertion of Error equality Two errors with different messages and/or names will no longer be asserted as equal. * Add test for error assertion * Enforce bearer auth scheme (#32) * Require Bearer prefix for auth token * Make Bearer check case insensitive * Fix tests * Add test for missing Bearer prefix error * Update docs with information about Bearer requirement * Fix style error
1 parent d019f84 commit 6f44a85

File tree

7 files changed

+60
-28
lines changed

7 files changed

+60
-28
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var Auth = require('lambda-foundation').authentication;
2727

2828
### Authentication
2929

30-
Authentication is an asynchronous process, that assumes the event contains a value under the `authorization` key. This value could be a pure OAuth token or it could be a full header (with type prefix). The API returns a promise that fails if the context is not properly authenticated. Upon success, the promise resolves the token into its claims, which in general contain a `sub`, `exp` and `iat` keys as per the [JWT spec](http://jwt.io/introduction/).
30+
Authentication is an asynchronous process, that assumes the event contains a value under the `authorization` key. This value must be an OAuth Bearer token, as defined in [RFC 6750](https://tools.ietf.org/html/rfc6750). The API returns a promise that fails if the context is not properly authenticated. Upon success, the promise resolves the token into its claims, which in general contain a `sub`, `exp` and `iat` keys as per the [JWT spec](http://jwt.io/introduction/).
3131

3232
```js
3333
var Auth = require('lambda-foundation').authentication;

lib/authentication/index.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ module.exports = {
3636
* @throws if token is invalid
3737
*/
3838
isValidToken: function(token) {
39-
if (!token) {
39+
if (!token || !_.startsWith(token.toUpperCase(), 'BEARER')) {
4040
throw new Error('401', 'Invalid token');
41-
}
42-
43-
if (_.startsWith(token, 'Bearer')) {
41+
} else {
4442
token = token.substring(6).trim();
4543
}
4644

lib/test/context.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ module.exports = {
7272
fail: function(err) {
7373
t.ok(true, 'Context finished with failure');
7474
if (expected) {
75-
t.same(err, expected, 'Context failed with expected result');
75+
if (err instanceof Error && err.name !== 'LambdaError') {
76+
t.same(err.name, expected.name, 'Context failed with expected error type');
77+
t.same(err.message, expected.message, 'Context failed with expected error message');
78+
} else {
79+
t.same(err, expected, 'Context failed with expected result');
80+
}
7681
}
7782
if(cb) {
7883
cb(err);

lib/test/event.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module.exports = function (extras){
2828
payload = _.isString(payload) ? {sub: payload} : payload;
2929

3030
return _.merge(
31-
{ authorization: jwt.sign(payload, secret) },
31+
{ authorization: 'Bearer ' + jwt.sign(payload, secret) },
3232
properties
3333
);
3434
},

test/authentication-test.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ tape.test('If invalid token then return 401', function(t) {
3434
tape.test('If valid token then return decoded token', function(t) {
3535

3636
try {
37-
const decoded = auth.isValidToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34');
37+
const decoded = auth.isValidToken('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34');
3838
t.equal(decoded.sub, '[email protected]');
3939
t.end();
4040
} catch (err) {
@@ -71,7 +71,7 @@ tape.test('If valid token with altered secret then return decoded token', functi
7171
tape.test('If valid token with Bearer keyword then return decoded token', function(t) {
7272

7373
try {
74-
const decoded = auth.isValidToken('Bearer ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34');
74+
const decoded = auth.isValidToken('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34');
7575
t.equal(decoded.sub, '[email protected]');
7676
t.end();
7777
} catch (err) {
@@ -163,7 +163,7 @@ tape.test('if valid token and valid scope then resolve decoded token', function(
163163
]
164164
};
165165

166-
const authPromise = auth.authenticate('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34', {
166+
const authPromise = auth.authenticate('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34', {
167167
scope: ['admin'],
168168
rule: auth.RULE.NONE
169169
});
@@ -179,7 +179,7 @@ tape.test('if valid token and valid scope then resolve decoded token', function(
179179

180180
tape.test('if invalid token and valid scope then reject', function(t) {
181181

182-
const authPromise = auth.authenticate('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34' + 'foo', {
182+
const authPromise = auth.authenticate('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34' + 'foo', {
183183
scope: ['tester'],
184184
rule: auth.RULE.NONE
185185
});
@@ -196,7 +196,7 @@ tape.test('if invalid token and valid scope then reject', function(t) {
196196

197197
tape.test('if valid token and invalid scope then reject', function(t) {
198198

199-
const authPromise = auth.authenticate('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34', {
199+
const authPromise = auth.authenticate('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34', {
200200
scope: ['admin']
201201
});
202202

@@ -209,3 +209,15 @@ tape.test('if valid token and invalid scope then reject', function(t) {
209209
t.end();
210210
});
211211
});
212+
213+
tape.test('if Bearer prefix is missing then reject', function(t) {
214+
try {
215+
auth.isValidToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwic2NvcGUiOlsidGVzdGVyIl19.ZzBZRdxQHFemCW2TwwFRn8Jk-uWt-OLtsi6O5pWpM34');
216+
217+
t.fail('didn\'t throw error');
218+
t.end();
219+
} catch (err) {
220+
t.equal(err.code, '401');
221+
t.end();
222+
}
223+
});

test/event-test.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const tape = require('tape');
33
const Event = require('../lib/test/event');
44
const jwt = require('jsonwebtoken');
5+
const auth = require('../lib/authentication');
56

67
tape.test('Should return authorized event with default secret', function(t) {
78

@@ -10,7 +11,7 @@ tape.test('Should return authorized event with default secret', function(t) {
1011
t.ok(event.authorization, 'Event has authorization defined');
1112

1213
try {
13-
jwt.verify(event.authorization, 'default_secret');
14+
auth.isValidToken(event.authorization);
1415
} catch(err) {
1516
t.fail('Unable to verify token');
1617
}
@@ -20,16 +21,20 @@ tape.test('Should return authorized event with default secret', function(t) {
2021

2122
tape.test('Should return authorized event with custom secret', function(t) {
2223

23-
const event = Event().authorized(null, 'secret');
24+
const testSecret = 'some-secret';
25+
26+
const event = Event().authorized(null, testSecret);
2427

2528
t.ok(event.authorization, 'Event has authorization defined');
2629

2730
try {
28-
jwt.verify(event.authorization, 'secret');
31+
auth.config({ secret: testSecret });
32+
auth.isValidToken(event.authorization);
2933
} catch(err) {
3034
t.fail('Unable to verify token');
3135
}
3236
t.same(event, {authorization: event.authorization}, 'Authorized event returned');
37+
auth.config({ secret: undefined }); // teardown because following tests use `default_secret`
3338
t.end();
3439
});
3540

@@ -41,7 +46,7 @@ tape.test('Should return authorized event with email payload', function(t) {
4146
t.ok(event.authorization, 'Event has authorization defined');
4247

4348
try {
44-
const decoded = jwt.verify(event.authorization, 'default_secret');
49+
const decoded = auth.isValidToken(event.authorization);
4550
t.same(decoded, {iat: decoded.iat, sub: '[email protected]'});
4651
} catch(err) {
4752
t.fail('Unable to verify token');
@@ -58,7 +63,7 @@ tape.test('Should return authorized event', function(t) {
5863
t.ok(event.authorization, 'Event has authorization defined');
5964

6065
try {
61-
const decoded = jwt.verify(event.authorization, 'default_secret');
66+
const decoded = auth.isValidToken(event.authorization);
6267
t.same(decoded, {iat: decoded.iat, sub: '[email protected]', scopes: ['admin']});
6368
} catch(err) {
6469
t.fail('Unable to verify token');
@@ -74,7 +79,7 @@ tape.test('Should return authorized event with extra properties', function(t) {
7479
t.ok(event.authorization, 'Event has authorization defined');
7580

7681
try {
77-
const decoded = jwt.verify(event.authorization, 'default_secret');
82+
const decoded = auth.isValidToken(event.authorization);
7883
t.same(decoded, {iat: decoded.iat});
7984
} catch(err) {
8085
t.fail('Unable to verify token');

test/mock-context-test.js

+22-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const tape = require('tape');
2-
const Error = require('../lib/error');
2+
const FoundationError = require('../lib/error');
33
const Context = require('../lib/test/context');
44

55
tape.test('Should succeed with expected result provided', function(t) {
@@ -25,35 +25,47 @@ tape.test('Should succeed with callback provided', function(t) {
2525
});
2626

2727
tape.test('Should fail with expected error provided', function(t) {
28-
const mock = Context.assertFail(t, new Error('999', 'Expected error'));
28+
const mock = Context.assertFail(t, new FoundationError('999', 'Expected error'));
2929
t.plan(2);
30-
mock.fail(new Error('999', 'Expected error'));
30+
mock.fail(new FoundationError('999', 'Expected error'));
3131
});
3232

3333
tape.test('Should fail with expected error and callback provided', function(t) {
34-
const mock = Context.assertFail(t, new Error('999', 'Expected error'), function(error) {
35-
t.same(error, new Error('999', 'Expected error'), 'Callback called');
34+
const mock = Context.assertFail(t, new FoundationError('999', 'Expected error'), function(error) {
35+
t.same(error, new FoundationError('999', 'Expected error'), 'Callback called');
3636
});
3737
t.plan(3);
38-
mock.fail(new Error('999', 'Expected error'));
38+
mock.fail(new FoundationError('999', 'Expected error'));
3939
});
4040

4141
tape.test('Should fail with expected error and callback provided', function(t) {
4242
const mock = Context.assertFail(t, function(error) {
43-
t.same(error, new Error('999', 'Expected error'), 'Callback called');
43+
t.same(error, new FoundationError('999', 'Expected error'), 'Callback called');
4444
});
4545
t.plan(2);
46-
mock.fail(new Error('999', 'Expected error'));
46+
mock.fail(new FoundationError('999', 'Expected error'));
4747
});
4848

4949
tape.test('Should succeed with done called', function(t) {
50-
const mock = Context.assertFail(t, new Error('999', 'Expected error'));
50+
const mock = Context.assertFail(t, new FoundationError('999', 'Expected error'));
5151
t.plan(2);
52-
mock.done(new Error('999', 'Expected error'));
52+
mock.done(new FoundationError('999', 'Expected error'));
5353
});
5454

5555
tape.test('Should succeed with done called', function(t) {
5656
const mock = Context.assertSucceed(t, {result:"result"});
5757
t.plan(2);
5858
mock.done(null, {result:"result"});
5959
});
60+
61+
tape.test('Should assert that two identical vanilla errors are equal', function(t) {
62+
const mock = Context.assertFail(t, new Error('Expected error'));
63+
mock.fail(new Error('Expected error'));
64+
});
65+
66+
tape.test('Should fail assertion if two vanilla errors are not the same', function(t) {
67+
t.same = t.notSame; // inverse assertion behaviour in context to show that its assertion did fail
68+
const mock = Context.assertFail(t, new Error('Expected error'));
69+
// mock is set to fail with error of different message and type, meaning that similarity assertion by Context should fail
70+
mock.fail(new RangeError('Unexpected error'));
71+
});

0 commit comments

Comments
 (0)