models: move AccessToken LDL def into a json file

This commit is contained in:
Miroslav Bajtoš 2014-10-13 10:23:35 +02:00
parent 1e6beabbd2
commit 5f20652241
4 changed files with 199 additions and 186 deletions

View File

@ -4,231 +4,202 @@
var loopback = require('../../lib/loopback') var loopback = require('../../lib/loopback')
, assert = require('assert') , assert = require('assert')
, crypto = require('crypto')
, uid = require('uid2') , uid = require('uid2')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
, DEFAULT_TOKEN_LEN = 64 , DEFAULT_TOKEN_LEN = 64
, Role = require('./role').Role , Role = require('./role').Role
, ACL = require('./acl').ACL; , ACL = require('./acl').ACL;
/*!
* Default AccessToken properties.
*/
var properties = {
id: {type: String, id: true},
ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds
created: {type: Date, default: function() {
return new Date();
}}
};
/** /**
* Token based authentication and access control. * Token based authentication and access control.
* *
* **Default ACLs** * **Default ACLs**
* *
* - DENY EVERYONE `*` * - DENY EVERYONE `*`
* - ALLOW EVERYONE create * - ALLOW EVERYONE create
* *
* @property {String} id Generated token ID * @property {String} id Generated token ID
* @property {Number} ttl Time to live in seconds * @property {Number} ttl Time to live in seconds, 2 weeks by default.
* @property {Date} created When the token was created * @property {Date} created When the token was created
* *
* @class * @class AccessToken
* @inherits {PersistedModel} * @inherits {PersistedModel}
*/ */
var AccessToken = module.exports = module.exports = function(AccessToken) {
loopback.PersistedModel.extend('AccessToken', properties, {
acls: [
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
permission: 'DENY'
},
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
property: 'create',
permission: 'ALLOW'
}
],
relations: {
user: {
type: 'belongsTo',
model: 'User',
foreignKey: 'userId'
}
}
});
/** // Workaround for https://github.com/strongloop/loopback/issues/292
* Anonymous Token AccessToken.definition.rawProperties.created.default =
* AccessToken.definition.properties.created.default = function() {
* ```js return new Date();
* assert(AccessToken.ANONYMOUS.id === '$anonymous'); };
* ```
*/
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'}); /**
* Anonymous Token
*
* ```js
* assert(AccessToken.ANONYMOUS.id === '$anonymous');
* ```
*/
/** AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
* Create a cryptographically random access token id.
*
* @callback {Function} callback
* @param {Error} err
* @param {String} token
*/
AccessToken.createAccessTokenId = function (fn) { /**
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) { * Create a cryptographically random access token id.
if(err) { *
fn(err); * @callback {Function} callback
} else { * @param {Error} err
fn(null, guid); * @param {String} token
} */
});
}
/*! AccessToken.createAccessTokenId = function(fn) {
* Hook to create accessToken id. uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
*/ if (err) {
fn(err);
AccessToken.beforeCreate = function (next, data) {
data = data || {};
AccessToken.createAccessTokenId(function (err, id) {
if(err) {
next(err);
} else {
data.id = id;
next();
}
});
}
/**
* Find a token for the given `ServerRequest`.
*
* @param {ServerRequest} req
* @param {Object} [options] Options for finding the token
* @callback {Function} callback
* @param {Error} err
* @param {AccessToken} token
*/
AccessToken.findForRequest = function(req, options, cb) {
var id = tokenIdForRequest(req, options);
if(id) {
this.findById(id, function(err, token) {
if(err) {
cb(err);
} else if(token) {
token.validate(function(err, isValid) {
if(err) {
cb(err);
} else if(isValid) {
cb(null, token);
} else {
var e = new Error('Invalid Access Token');
e.status = e.statusCode = 401;
cb(e);
}
});
} else { } else {
cb(); fn(null, guid);
} }
}); });
} else { }
process.nextTick(function() {
cb(); /*!
* Hook to create accessToken id.
*/
AccessToken.beforeCreate = function(next, data) {
data = data || {};
AccessToken.createAccessTokenId(function(err, id) {
if (err) {
next(err);
} else {
data.id = id;
next();
}
}); });
} }
}
/** /**
* Validate the token. * Find a token for the given `ServerRequest`.
* *
* @callback {Function} callback * @param {ServerRequest} req
* @param {Error} err * @param {Object} [options] Options for finding the token
* @param {Boolean} isValid * @callback {Function} callback
*/ * @param {Error} err
* @param {AccessToken} token
*/
AccessToken.prototype.validate = function(cb) { AccessToken.findForRequest = function(req, options, cb) {
try { var id = tokenIdForRequest(req, options);
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(); if (id) {
var created = this.created.getTime(); this.findById(id, function(err, token) {
var elapsedSeconds = (now - created) / 1000; if (err) {
var secondsToLive = this.ttl; cb(err);
var isValid = elapsedSeconds < secondsToLive; } else if (token) {
token.validate(function(err, isValid) {
if(isValid) { if (err) {
cb(null, isValid); cb(err);
} else if (isValid) {
cb(null, token);
} else {
var e = new Error('Invalid Access Token');
e.status = e.statusCode = 401;
cb(e);
}
});
} else {
cb();
}
});
} else { } else {
this.destroy(function(err) { process.nextTick(function() {
cb(err, isValid); cb();
}); });
} }
} catch(e) {
cb(e);
}
}
function tokenIdForRequest(req, options) {
var params = options.params || [];
var headers = options.headers || [];
var cookies = options.cookies || [];
var i = 0;
var length;
var id;
params = params.concat(['access_token']);
headers = headers.concat(['X-Access-Token', 'authorization']);
cookies = cookies.concat(['access_token', 'authorization']);
for(length = params.length; i < length; i++) {
id = req.param(params[i]);
if(typeof id === 'string') {
return id;
}
} }
for(i = 0, length = headers.length; i < length; i++) { /**
id = req.header(headers[i]); * Validate the token.
*
* @callback {Function} callback
* @param {Error} err
* @param {Boolean} isValid
*/
if(typeof id === 'string') { AccessToken.prototype.validate = function(cb) {
// Add support for oAuth 2.0 bearer token try {
// http://tools.ietf.org/html/rfc6750 assert(
if (id.indexOf('Bearer ') === 0) { this.created && typeof this.created.getTime === 'function',
id = id.substring(7); 'token.created must be a valid Date'
// Decode from base64 );
var buf = new Buffer(id, 'base64'); assert(this.ttl !== 0, 'token.ttl must be not be 0');
id = buf.toString('utf8'); 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);
});
} }
return id; } catch (e) {
cb(e);
} }
} }
if(req.signedCookies) { function tokenIdForRequest(req, options) {
for(i = 0, length = cookies.length; i < length; i++) { var params = options.params || [];
id = req.signedCookies[cookies[i]]; var headers = options.headers || [];
var cookies = options.cookies || [];
var i = 0;
var length;
var id;
if(typeof id === 'string') { params = params.concat(['access_token']);
headers = headers.concat(['X-Access-Token', 'authorization']);
cookies = cookies.concat(['access_token', 'authorization']);
for (length = params.length; i < length; i++) {
id = req.param(params[i]);
if (typeof id === 'string') {
return id; return id;
} }
} }
for (i = 0, length = headers.length; i < length; i++) {
id = req.header(headers[i]);
if (typeof id === 'string') {
// Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750
if (id.indexOf('Bearer ') === 0) {
id = id.substring(7);
// Decode from base64
var buf = new Buffer(id, 'base64');
id = buf.toString('utf8');
}
return id;
}
}
if (req.signedCookies) {
for (i = 0, length = cookies.length; i < length; i++) {
id = req.signedCookies[cookies[i]];
if (typeof id === 'string') {
return id;
}
}
}
return null;
} }
return null; };
}

View File

@ -0,0 +1,38 @@
{
"name": "AccessToken",
"properties": {
"id": {
"type": "string",
"id": true
},
"ttl": {
"type": "number",
"ttl": true,
"default": 1209600,
"description": "time to live in seconds (2 weeks by default)"
},
"created": {
"type": "Date"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": "userId"
}
},
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"principalType": "ROLE",
"principalId": "$everyone",
"property": "create",
"permission": "ALLOW"
}
]
}

View File

@ -1,5 +1,4 @@
var loopback = require('./loopback'); var loopback = require('./loopback');
var AccessToken = require('../common/models/access-token');
var debug = require('debug')('loopback:security:access-context'); var debug = require('debug')('loopback:security:access-context');
/** /**
@ -49,7 +48,9 @@ function AccessContext(context) {
} }
this.accessType = context.accessType || AccessContext.ALL; this.accessType = context.accessType || AccessContext.ALL;
this.accessToken = context.accessToken || AccessToken.ANONYMOUS; assert(loopback.AccessToken,
'AccessToken model must be defined before AccessContext model');
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
var principalType = context.principalType || Principal.USER; var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined; var principalId = context.principalId || undefined;

View File

@ -9,7 +9,10 @@ module.exports = function(loopback) {
require('../common/models/application.json'), require('../common/models/application.json'),
require('../common/models/application.js')); require('../common/models/application.js'));
loopback.AccessToken = require('../common/models/access-token'); loopback.AccessToken = createModel(
require('../common/models/access-token.json'),
require('../common/models/access-token.js'));
loopback.Role = require('../common/models/role').Role; loopback.Role = require('../common/models/role').Role;
loopback.RoleMapping = require('../common/models/role').RoleMapping; loopback.RoleMapping = require('../common/models/role').RoleMapping;
loopback.ACL = require('../common/models/acl').ACL; loopback.ACL = require('../common/models/acl').ACL;