diff --git a/common/models/access-token.js b/common/models/access-token.js index 8243747d..bfb9cb6a 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -4,231 +4,202 @@ var loopback = require('../../lib/loopback') , assert = require('assert') - , crypto = require('crypto') , uid = require('uid2') - , DEFAULT_TTL = 1209600 // 2 weeks in seconds , DEFAULT_TOKEN_LEN = 64 , Role = require('./role').Role , 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. * * **Default ACLs** - * + * * - DENY EVERYONE `*` * - ALLOW EVERYONE create * * @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 - * - * @class + * + * @class AccessToken * @inherits {PersistedModel} */ -var AccessToken = module.exports = - 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' - } - } -}); +module.exports = function(AccessToken) { -/** - * Anonymous Token - * - * ```js - * assert(AccessToken.ANONYMOUS.id === '$anonymous'); - * ``` - */ + // Workaround for https://github.com/strongloop/loopback/issues/292 + AccessToken.definition.rawProperties.created.default = + AccessToken.definition.properties.created.default = function() { + return new Date(); + }; -AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'}); + /** + * Anonymous Token + * + * ```js + * assert(AccessToken.ANONYMOUS.id === '$anonymous'); + * ``` + */ -/** - * Create a cryptographically random access token id. - * - * @callback {Function} callback - * @param {Error} err - * @param {String} token - */ + AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'}); -AccessToken.createAccessTokenId = function (fn) { - uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) { - if(err) { - fn(err); - } else { - fn(null, guid); - } - }); -} + /** + * Create a cryptographically random access token id. + * + * @callback {Function} callback + * @param {Error} err + * @param {String} token + */ -/*! - * 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(); - } - }); -} - -/** - * 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); - } - }); + AccessToken.createAccessTokenId = function(fn) { + uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) { + if (err) { + fn(err); } 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. - * - * @callback {Function} callback - * @param {Error} err - * @param {Boolean} isValid - */ + /** + * 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.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'); + AccessToken.findForRequest = function(req, options, cb) { + var id = tokenIdForRequest(req, options); - 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); + 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 { + cb(); + } + }); } else { - this.destroy(function(err) { - cb(err, isValid); + process.nextTick(function() { + 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') { - // 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'); + 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); + }); } - return id; + } catch (e) { + cb(e); } } - if(req.signedCookies) { - for(i = 0, length = cookies.length; i < length; i++) { - id = req.signedCookies[cookies[i]]; + function tokenIdForRequest(req, options) { + var params = options.params || []; + 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; } } + + 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; -} +}; diff --git a/common/models/access-token.json b/common/models/access-token.json new file mode 100644 index 00000000..a5f360c4 --- /dev/null +++ b/common/models/access-token.json @@ -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" + } + ] +} diff --git a/lib/access-context.js b/lib/access-context.js index ce8457f1..89edebaf 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -1,5 +1,4 @@ var loopback = require('./loopback'); -var AccessToken = require('../common/models/access-token'); var debug = require('debug')('loopback:security:access-context'); /** @@ -49,7 +48,9 @@ function AccessContext(context) { } 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 principalId = context.principalId || undefined; diff --git a/lib/builtin-models.js b/lib/builtin-models.js index ef847a07..23a27de3 100644 --- a/lib/builtin-models.js +++ b/lib/builtin-models.js @@ -9,7 +9,10 @@ module.exports = function(loopback) { require('../common/models/application.json'), 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.RoleMapping = require('../common/models/role').RoleMapping; loopback.ACL = require('../common/models/acl').ACL;