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
, 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 || [];

View File

@ -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);

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

View File

@ -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);