From 1de2a40e88fc2b54b7deeb9322e2eb0ce6ad00f4 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Thu, 14 Nov 2013 18:34:51 -0800 Subject: [PATCH] Update AccessToken and User relationship - Add created default - Default TTLs for user login access tokens - Break out User / AccessToken relationship --- lib/models/access-token.js | 53 +++++++++++++++++++++++++++++++++++--- lib/models/user.js | 33 ++++++++++-------------- test/access-token.test.js | 21 +++++++++++++++ test/user.test.js | 11 ++++---- 4 files changed, 89 insertions(+), 29 deletions(-) diff --git a/lib/models/access-token.js b/lib/models/access-token.js index b8b39c4b..ec2f0187 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -4,8 +4,10 @@ var Model = require('../loopback').Model , loopback = require('../loopback') + , assert = require('assert') , crypto = require('crypto') , uid = require('uid2') + , DEFAULT_TTL = 1209600 // 2 weeks in seconds , DEFAULT_TOKEN_LEN = 64; /** @@ -14,9 +16,10 @@ var Model = require('../loopback').Model var properties = { id: {type: String, generated: true, id: 1}, - uid: {type: String}, - ttl: {type: Number, ttl: true}, // time to live in seconds - created: {type: Date} + ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds + created: {type: Date, default: function() { + return new Date(); + }} }; /** @@ -71,7 +74,21 @@ AccessToken.findForRequest = function(req, options, cb) { var id = tokenIdForRequest(req, options); if(id) { - this.findById(id, cb); + this.findById(id, function(err, token) { + if(err) { + cb(err); + } else { + token.validate(function(err, isValid) { + if(err) { + cb(err); + } else if(isValid) { + cb(null, token); + } else { + cb(new Error('Invalid Access Token')); + } + }); + } + }); } else { process.nextTick(function() { cb(); @@ -79,6 +96,34 @@ AccessToken.findForRequest = function(req, options, cb) { } } +AccessToken.prototype.validate = function(cb) { + try { + assert( + this.created && typeof this.created.getTime === 'function', + 'token.created must be a valid Date' + ); + assert(this.ttl !== 0, 'token.ttl must be not be 0'); + assert(this.ttl, 'token.ttl must exist'); + assert(this.ttl >= -1, 'token.ttl must be >= -1'); + + var now = Date.now(); + var created = this.created.getTime(); + var elapsedSeconds = (now - created) / 1000; + var secondsToLive = this.ttl; + var isValid = elapsedSeconds < secondsToLive; + + if(isValid) { + cb(null, isValid); + } else { + this.destroy(function(err) { + cb(err, isValid); + }); + } + } catch(e) { + cb(e); + } +} + function tokenIdForRequest(req, options) { var params = options.params || []; var headers = options.headers || []; diff --git a/lib/models/user.js b/lib/models/user.js index dc4685a4..ace3ef5a 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -9,7 +9,10 @@ var Model = require('../loopback').Model , crypto = require('crypto') , bcrypt = require('bcryptjs') , passport = require('passport') - , LocalStrategy = require('passport-local').Strategy; + , LocalStrategy = require('passport-local').Strategy + , BaseAccessToken = require('./access-token') + , DEFAULT_TTL = 1209600 // 2 weeks in seconds + , DEFAULT_MAX_TTL = 31556926; // 1 year in seconds /** * Default User properties. @@ -79,7 +82,9 @@ User.login = function (credentials, fn) { if(err) { fn(defaultError); } else if(isMatch) { - createAccessToken(user, fn); + user.accessTokens.create({ + ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) + }, fn); } else { fn(defaultError); } @@ -88,18 +93,6 @@ User.login = function (credentials, fn) { fn(defaultError); } }); - - function createAccessToken(user, fn) { - var AccessToken = UserCtor.accessToken; - - AccessToken.create({uid: user.id}, function (err, accessToken) { - if(err) { - fn(err); - } else { - fn(null, accessToken) - } - }); - } } /** @@ -112,12 +105,8 @@ User.login = function (credentials, fn) { * @param {String} accessTokenID */ -User.logout = function (sid, fn) { - var UserCtor = this; - - var AccessToken = UserCtor.settings.accessToken || loopback.AccessToken; - - AccessToken.findById(sid, function (err, accessToken) { +User.logout = function (tokenId, fn) { + this.relations.accessTokens.modelTo.findById(tokenId, function (err, accessToken) { if(err) { fn(err); } else if(accessToken) { @@ -255,6 +244,10 @@ User.setup = function () { Model.setup.call(this); var UserModel = this; + // max ttl + this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; + this.settings.ttl = DEFAULT_TTL; + UserModel.setter.password = function (plain) { var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); this.$password = bcrypt.hashSync(plain, salt); diff --git a/test/access-token.test.js b/test/access-token.test.js index f76829ef..d74ef153 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -36,6 +36,27 @@ describe('loopback.token(options)', function() { }); }); +describe('AccessToken', function () { + beforeEach(createTestingToken); + + it('should auto-generate id', function () { + assert(this.token.id); + assert.equal(this.token.id.length, 64); + }); + + it('should auto-generate created date', function () { + assert(this.token.created); + assert(Object.prototype.toString.call(this.token.created), '[object Date]'); + }); + + it('should be validateable', function (done) { + this.token.validate(function(err, isValid) { + assert(isValid); + done(); + }); + }); +}); + function createTestingToken(done) { var test = this; Token.create({}, function (err, token) { diff --git a/test/user.test.js b/test/user.test.js index 24a7f7b9..74ddab1a 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -7,7 +7,6 @@ var userMemory = loopback.createDataSource({ connector: loopback.Memory }); - describe('User', function(){ var mailDataSource = loopback.createDataSource({ @@ -15,7 +14,9 @@ describe('User', function(){ transports: [{type: 'STUB'}] }); User.attachTo(userMemory); - User.accessToken.attachTo(userMemory); + AccessToken.attachTo(userMemory); + // TODO(ritch) - this should be a default relationship + User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); User.email.attachTo(mailDataSource); // allow many User.afterRemote's to be called @@ -101,7 +102,7 @@ describe('User', function(){ describe('User.login', function() { it('Login a user by providing credentials', function(done) { User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) { - assert(accessToken.uid); + assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); @@ -119,7 +120,7 @@ describe('User', function(){ if(err) return done(err); var accessToken = res.body; - assert(accessToken.uid); + assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); @@ -164,7 +165,7 @@ describe('User', function(){ if(err) return done(err); var accessToken = res.body; - assert(accessToken.uid); + assert(accessToken.userId); assert(accessToken.id); fn(null, accessToken.id);