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:
parent
efce5039f6
commit
1de2a40e88
|
@ -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 || [];
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue