Update AccessToken and User relationship

- Add created default
 - Default TTLs for user login access tokens
 - Break out User / AccessToken relationship
This commit is contained in:
Ritchie Martori 2013-11-14 18:34:51 -08:00
parent efce5039f6
commit 1de2a40e88
4 changed files with 89 additions and 29 deletions

View File

@ -4,8 +4,10 @@
var Model = require('../loopback').Model var Model = require('../loopback').Model
, loopback = require('../loopback') , loopback = require('../loopback')
, assert = require('assert')
, crypto = require('crypto') , crypto = require('crypto')
, uid = require('uid2') , uid = require('uid2')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
, DEFAULT_TOKEN_LEN = 64; , DEFAULT_TOKEN_LEN = 64;
/** /**
@ -14,9 +16,10 @@ var Model = require('../loopback').Model
var properties = { var properties = {
id: {type: String, generated: true, id: 1}, id: {type: String, generated: true, id: 1},
uid: {type: String}, ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds
ttl: {type: Number, ttl: true}, // time to live in seconds created: {type: Date, default: function() {
created: {type: Date} return new Date();
}}
}; };
/** /**
@ -71,7 +74,21 @@ AccessToken.findForRequest = function(req, options, cb) {
var id = tokenIdForRequest(req, options); var id = tokenIdForRequest(req, options);
if(id) { 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 { } else {
process.nextTick(function() { process.nextTick(function() {
cb(); 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) { function tokenIdForRequest(req, options) {
var params = options.params || []; var params = options.params || [];
var headers = options.headers || []; var headers = options.headers || [];

View File

@ -9,7 +9,10 @@ var Model = require('../loopback').Model
, crypto = require('crypto') , crypto = require('crypto')
, bcrypt = require('bcryptjs') , bcrypt = require('bcryptjs')
, passport = require('passport') , 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. * Default User properties.
@ -79,7 +82,9 @@ User.login = function (credentials, fn) {
if(err) { if(err) {
fn(defaultError); fn(defaultError);
} else if(isMatch) { } else if(isMatch) {
createAccessToken(user, fn); user.accessTokens.create({
ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
}, fn);
} else { } else {
fn(defaultError); fn(defaultError);
} }
@ -88,18 +93,6 @@ User.login = function (credentials, fn) {
fn(defaultError); 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 * @param {String} accessTokenID
*/ */
User.logout = function (sid, fn) { User.logout = function (tokenId, fn) {
var UserCtor = this; this.relations.accessTokens.modelTo.findById(tokenId, function (err, accessToken) {
var AccessToken = UserCtor.settings.accessToken || loopback.AccessToken;
AccessToken.findById(sid, function (err, accessToken) {
if(err) { if(err) {
fn(err); fn(err);
} else if(accessToken) { } else if(accessToken) {
@ -255,6 +244,10 @@ User.setup = function () {
Model.setup.call(this); Model.setup.call(this);
var UserModel = 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) { UserModel.setter.password = function (plain) {
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
this.$password = bcrypt.hashSync(plain, salt); this.$password = bcrypt.hashSync(plain, salt);

View File

@ -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) { function createTestingToken(done) {
var test = this; var test = this;
Token.create({}, function (err, token) { Token.create({}, function (err, token) {

View File

@ -7,7 +7,6 @@ var userMemory = loopback.createDataSource({
connector: loopback.Memory connector: loopback.Memory
}); });
describe('User', function(){ describe('User', function(){
var mailDataSource = loopback.createDataSource({ var mailDataSource = loopback.createDataSource({
@ -15,7 +14,9 @@ describe('User', function(){
transports: [{type: 'STUB'}] transports: [{type: 'STUB'}]
}); });
User.attachTo(userMemory); 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); User.email.attachTo(mailDataSource);
// allow many User.afterRemote's to be called // allow many User.afterRemote's to be called
@ -101,7 +102,7 @@ describe('User', function(){
describe('User.login', function() { describe('User.login', function() {
it('Login a user by providing credentials', function(done) { it('Login a user by providing credentials', function(done) {
User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) { User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) {
assert(accessToken.uid); assert(accessToken.userId);
assert(accessToken.id); assert(accessToken.id);
assert.equal(accessToken.id.length, 64); assert.equal(accessToken.id.length, 64);
@ -119,7 +120,7 @@ describe('User', function(){
if(err) return done(err); if(err) return done(err);
var accessToken = res.body; var accessToken = res.body;
assert(accessToken.uid); assert(accessToken.userId);
assert(accessToken.id); assert(accessToken.id);
assert.equal(accessToken.id.length, 64); assert.equal(accessToken.id.length, 64);
@ -164,7 +165,7 @@ describe('User', function(){
if(err) return done(err); if(err) return done(err);
var accessToken = res.body; var accessToken = res.body;
assert(accessToken.uid); assert(accessToken.userId);
assert(accessToken.id); assert(accessToken.id);
fn(null, accessToken.id); fn(null, accessToken.id);