models: move AccessToken LDL def into a json file
This commit is contained in:
parent
1e6beabbd2
commit
5f20652241
|
@ -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;
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue