From bfb154d445980aebbcb611b998b8cc210ba9c958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 4 Feb 2014 16:17:32 +0100 Subject: [PATCH] Modify `loopback.rest` to include `loopback.token` Make `loopback.rest` self-contained, so that authentication works out of the box. var app = loopback(); app.enableAuth(); app.use(loopback.rest()); Note that cookie parsing middleware is not added, users have to explicitly configure that if they want to store access tokens in cookies. Modify `loopback.token` to skip token lookup when the request already contains `accessToken` property. This is in line with other connect-based middleware like `cookieParser` or `json`. --- lib/middleware/rest.js | 24 +++++++++- lib/middleware/token.js | 2 + test/access-token.test.js | 21 +++++++++ test/rest.middleware.test.js | 86 ++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 test/rest.middleware.test.js diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js index 9ddef750..3f87c126 100644 --- a/lib/middleware/rest.js +++ b/lib/middleware/rest.js @@ -15,17 +15,37 @@ module.exports = rest; */ function rest() { + var tokenParser = null; return function (req, res, next) { var app = req.app; var handler = app.handler('rest'); - + if(req.url === '/routes') { res.send(handler.adapter.allRoutes()); } else if(req.url === '/models') { return res.send(app.remotes().toJSON()); + } else if (app.isAuthEnabled) { + if (!tokenParser) { + // NOTE(bajtos) It would be better to search app.models for a model + // of type AccessToken instead of searching all loopback models. + // Unfortunately that's not supported now. + // Related discussions: + // https://github.com/strongloop/loopback/pull/167 + // https://github.com/strongloop/loopback/commit/f07446a + var AccessToken = loopback.getModelByType(loopback.AccessToken); + tokenParser = loopback.token({ model: AccessToken }); + } + + tokenParser(req, res, function(err) { + if (err) { + next(err); + } else { + handler(req, res, next); + } + }); } else { handler(req, res, next); } - } + }; } diff --git a/lib/middleware/token.js b/lib/middleware/token.js index 185a71ce..88a93c24 100644 --- a/lib/middleware/token.js +++ b/lib/middleware/token.js @@ -53,12 +53,14 @@ function token(options) { assert(TokenModel, 'loopback.token() middleware requires a AccessToken model'); return function (req, res, next) { + if (req.accessToken !== undefined) return next(); TokenModel.findForRequest(req, options, function(err, token) { if(err) return next(err); if(token) { req.accessToken = token; next(); } else { + req.accessToken = null; return next(); } }); diff --git a/test/access-token.test.js b/test/access-token.test.js index 0ac52b49..50cf4d9a 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -32,6 +32,27 @@ describe('loopback.token(options)', function() { .end(done); }); }); + + it('should skip when req.token is already present', function(done) { + var tokenStub = { id: 'stub id' }; + app.use(function(req, res, next) { + req.accessToken = tokenStub; + next(); + }); + app.use(loopback.token({ model: Token })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', this.token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); + }); + }); }); describe('AccessToken', function () { diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js new file mode 100644 index 00000000..3c1e9886 --- /dev/null +++ b/test/rest.middleware.test.js @@ -0,0 +1,86 @@ +describe('loopback.rest', function() { + beforeEach(function() { + app.dataSource('db', { connector: loopback.Memory }); + }); + + it('works out-of-the-box', function(done) { + app.model('MyModel', { dataSource: 'db' }); + app.use(loopback.rest()); + request(app).get('/mymodels') + .expect(200) + .end(done); + }); + + it('includes loopback.token when necessary', function(done) { + givenUserModelWithAuth(); + app.enableAuth(); + app.use(loopback.rest()); + + givenLoggedInUser(function(err, token) { + if (err) return done(err); + expect(token).instanceOf(app.models.accessToken); + request(app).get('/users/' + token.userId) + .set('Authorization', token.id) + .expect(200) + .end(done); + }); + }); + + it('does not include loopback.token when auth not enabled', function(done) { + var User = givenUserModelWithAuth(); + User.getToken = function(req, cb) { + cb(null, req.accessToken ? req.accessToken.id : null); + }; + loopback.remoteMethod(User.getToken, { + accepts: [{ type: 'object', http: { source: 'req' } }], + returns: [{ type: 'object', name: 'id' }] + }); + + app.use(loopback.rest()); + givenLoggedInUser(function(err, token) { + if (err) return done(err); + request(app).get('/users/getToken') + .set('Authorization', token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + expect(res.body.id).to.equal(null); + done(); + }); + }); + }); + + function givenUserModelWithAuth() { + // NOTE(bajtos) It is important to create a custom AccessToken model here, + // in order to overwrite the entry created by previous tests in + // the global model registry + app.model('accessToken', { + options: { + base: 'AccessToken' + }, + dataSource: 'db' + }); + return app.model('user', { + options: { + base: 'User', + relations: { + accessTokens: { + model: 'accessToken', + type: 'hasMany', + foreignKey: 'userId' + } + } + }, + dataSource: 'db' + }); + } + function givenLoggedInUser(cb) { + var credentials = { email: 'user@example.com', password: 'pwd' }; + var User = app.models.user; + User.create(credentials, + function(err, user) { + if (err) return done(err); + User.login(credentials, cb); + }); + } +});