Browse files

Issue #19299 better working code push.

  • Loading branch information...
1 parent 22a6f03 commit d59e26f33cd63d41e0eed517aa681554605911ad @bendiy bendiy committed Mar 6, 2013
View
1 lib/backbone-x/source/package.js 100644 → 100755
@@ -3,6 +3,7 @@ depends(
"ext",
"core.js",
"model.js",
+ "simplemodel.js",
"collection.js",
"document.js",
"info.js",
View
434 lib/backbone-x/source/simplemodel.js
@@ -0,0 +1,434 @@
+// Contributions of status related functionality borrowed from SproutCore:
+// https://github.com/sproutcore/sproutcore
+
+/*jshint indent:2, curly:true eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true */
+
+(function () {
+ "use strict";
+
+ /**
+ @class `XM.Model` is an abstract class designed to operate with `XT.DataSource`.
+ It should be subclassed for any specific implementation. Subclasses should
+ include a `recordType` the data source will use to retrieve the record.
+
+ To create a new model include `isNew` in the options:
+ <pre><code>
+ // Create a new class
+ XM.MyModel = XM.Model.extend({
+ recordType: 'XM.MyModel'
+ });
+
+ // Instantiate a new model object
+ m = new XM.MyModel(null, {isNew: true});
+ </code></pre>
+ To load an existing record include an id in the attributes:
+ <pre><code>
+ m = new XM.MyModel({id: 1});
+ m.fetch();
+ </code></pre>
+
+ @name XM.Model
+ @description To create a new model include `isNew` in the options:
+ @param {Object} Attributes
+ @param {Object} Options
+ @extends Backbone.RelationalModel
+ */
+ XM.SimpleModel = Backbone.Model.extend(/** @lends XM.Model# */{
+
+ /**
+ Set to true if you want an id fetched from the server when the `isNew` option
+ is passed on a new model.
+
+ @type {Boolean}
+ */
+ autoFetchId: true,
+
+ /**
+ Differentiates models that belong to postbooks instances versus models
+ that belong to the global database
+ */
+ databaseType: 'instance',
+
+ /**
+ The last error message reported.
+ */
+ lastError: null,
+
+ /**
+ Specify the name of a data source model here.
+
+ @type {String}
+ */
+ recordType: null,
+
+ /**
+ Model's status. You should never modify this directly.
+
+ @seealso `getStatus`
+ @seealse `setStatus`
+ @type {Number}
+ @default `EMPTY`
+ */
+ status: null,
+
+ // ..........................................................
+ // METHODS
+ //
+
+ /**
+ Returns only attribute records that have changed.
+
+ @type Hash
+ */
+ changeSet: function () {
+ var attributes = this.toJSON();
+
+ // recursive function that does the work
+ var changedOnly = function (attrs) {
+ var ret = null,
+ i,
+ prop,
+ val;
+ if (attrs && attrs.dataState !== 'read') {
+ ret = {};
+ for (prop in attrs) {
+ if (attrs[prop] instanceof Array) {
+ ret[prop] = [];
+ // loop through array and only include dirty items
+ for (i = 0; i < attrs[prop].length; i++) {
+ val = changedOnly(attrs[prop][i]);
+ if (val) {ret[prop].push(val); }
+ }
+ } else {
+ ret[prop] = attrs[prop];
+ }
+ }
+ }
+ return ret;
+ };
+
+ // do the work
+ return changedOnly(attributes);
+ },
+
+ /**
+ Update the status if applicable.
+ */
+ didChange: function (model, options) {
+ model = model || {};
+ options = options || {};
+ var K = XM.Model,
+ status = this.getStatus();
+ if (options.force) { return; }
+
+ // Mark dirty if we should
+ if (status === K.READY_CLEAN) {
+ this.setStatus(K.READY_DIRTY);
+ }
+ },
+
+ /**
+ Called after confirmation that the model was destroyed on the
+ data source.
+ */
+ didDestroy: function () {
+ var K = XM.Model;
+ this.setStatus(K.DESTROYED_CLEAN);
+ this.clear({silent: true});
+ },
+
+ /**
+ Handle a `sync` response that was an error.
+ */
+ didError: function (model, resp) {
+ model = model || {};
+ this.lastError = resp;
+ XT.log(resp);
+ },
+
+ /**
+ Reimplemented to handle state change and parent child relationships. Calling
+ `destroy` on a parent will cause the model to commit to the server
+ immediately. Calling destroy on a child relation will simply mark it for
+ deletion on the next save of the parent.
+
+ @returns {XT.Request|Boolean}
+ */
+ destroy: function (options) {
+ var result,
+ K = XM.Model;
+
+ this._wasNew = this.isNew(); // Hack so prototype call will still work
+ this.setStatus(K.BUSY_DESTROYING);
+ options.wait = true;
+ result = Backbone.Model.prototype.destroy.call(this, options);
+ delete this._wasNew;
+ return result;
+ },
+
+ /*
+ Forward a dispatch request to the data source. Runs a "dispatchable" database function.
+ Include a `success` callback function in options to handle the result.
+
+ @param {String} Name of the class
+ @param {String} Function name
+ @param {Object} Parameters
+ @param {Object} Options
+ */
+ dispatch: function (name, func, params, options) {
+ options = options ? _.clone(options) : {};
+ if (!options.databaseType) { options.databaseType = this.databaseType; }
+ var dataSource = options.dataSource || XT.dataSource;
+ return dataSource.dispatch(name, func, params, options);
+ },
+
+ /*
+ Reimplemented to handle status changes.
+
+ @param {Object} Options
+ @returns {XT.Request} Request
+ */
+ fetch: function (options) {
+ options = options ? _.clone(options) : {};
+ var model = this,
+ K = XM.Model,
+ success = options.success;
+
+ this.setStatus(K.BUSY_FETCHING);
+ options.success = function (resp) {
+ model.setStatus(K.READY_CLEAN, options);
+ if (XT.debugging) {
+ XT.log('Fetch successful');
+ }
+ if (success) { success(model, resp, options); }
+ };
+ return Backbone.Model.prototype.fetch.call(this, options);
+ },
+
+ /**
+ Set the id on this record an id from the server. Including the `cascade`
+ option will call ids to be fetched recursively for `HasMany` relations.
+
+ @returns {XT.Request} Request
+ */
+ fetchId: function (options) {
+ options = _.defaults(options ? _.clone(options) : {}, {force: true});
+ var that = this;
+ if (!this.id) {
+ options.success = function (resp) {
+ options.force = true;
+ that.set(that.idAttribute, resp, options);
+ };
+ this.dispatch('XM.Model', 'fetchId', this.recordType, options);
+ }
+ },
+
+ /**
+ Return the current status.
+
+ @returns {Number}
+ */
+ getStatus: function () {
+ return this.status;
+ },
+
+ /**
+ Return the current status as as string.
+
+ @returns {String}
+ */
+ getStatusString: function () {
+ var ret = [],
+ status = this.getStatus(),
+ prop;
+ for (prop in XM.Model) {
+ if (XM.Model.hasOwnProperty(prop)) {
+ if (prop.match(/[A-Z_]$/) && XM.Model[prop] === status) {
+ ret.push(prop);
+ }
+ }
+ }
+ return ret.join(" ");
+ },
+
+ /**
+ Called when model is instantiated.
+ */
+ initialize: function (attributes, options) {
+ attributes = attributes || {};
+ options = options || {};
+ var K = XM.Model,
+ status = this.getStatus();
+
+ // Validate
+ if (_.isEmpty(this.recordType)) { throw 'No record type defined'; }
+ if (status !== K.EMPTY) {
+ throw 'Model may only be intialized from a status of EMPTY.';
+ }
+
+ // Handle options
+ if (options.isNew) {
+ this.setStatus(K.READY_NEW);
+ if (this.autoFetchId) { this.fetchId(); }
+ } else if (options.force) {
+ this.setStatus(K.READY_CLEAN);
+ }
+
+ // Bind events
+ this.on('change', this.didChange);
+ this.on('error', this.didError);
+ this.on('destroy', this.didDestroy);
+ },
+
+ /**
+ Reimplemented. A model is new if the status is `READY_NEW`.
+
+ @returns {Boolean}
+ */
+ isNew: function () {
+ var K = XM.Model;
+ return this.getStatus() === K.READY_NEW || this._wasNew || false;
+ },
+
+ /**
+ Returns true if status is `READY_NEW` or `READY_DIRTY`.
+
+ @returns {Boolean}
+ */
+ isDirty: function () {
+ var status = this.getStatus(),
+ K = XM.Model;
+ return status === K.READY_NEW || status === K.READY_DIRTY;
+ },
+
+ /**
+ Revert the model to the previous status. Useful for reseting status
+ after a failed validation.
+
+ param {Boolean} - cascade
+ */
+ revertStatus: function (cascade) {
+ var K = XM.Model,
+ prev = this._prevStatus;
+ this.setStatus(this._prevStatus || K.EMPTY);
+ this._prevStatus = prev;
+ },
+
+ /**
+ Reimplemented.
+
+ @retuns {XT.Request} Request
+ */
+ save: function (key, value, options) {
+ options = options ? _.clone(options) : {};
+ var attrs = {},
+ model = this,
+ K = XM.Model,
+ success,
+ result;
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ if (_.isObject(key) || _.isEmpty(key)) {
+ attrs = key;
+ options = value ? _.clone(value) : {};
+ } else if (_.isString(key)) {
+ attrs[key] = value;
+ }
+
+ // Only save if we should.
+ if (this.isDirty() || attrs) {
+ success = options.success;
+ options.wait = true;
+ options.validateSave = true;
+ options.success = function (resp) {
+ model.setStatus(K.READY_CLEAN, options);
+ if (XT.debugging) {
+ XT.log('Save successful');
+ }
+ if (success) { success(model, resp, options); }
+ };
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ if (_.isObject(key) || _.isEmpty(key)) { value = options; }
+
+ // Call the super version
+ this.setStatus(K.BUSY_COMMITTING);
+ result = Backbone.Model.prototype.save.call(this, key, value, options);
+ if (!result) { this.revertStatus(true); }
+ return result;
+ }
+
+ XT.log('No changes to save');
+ return false;
+ },
+
+ /**
+ Set the status on the model. Triggers `statusChange` event. Option set to
+ `cascade` will propagate status recursively to all HasMany children.
+
+ @param {Number} Status
+ */
+ setStatus: function (status, options) {
+ var K = XM.Model,
+ setOptions = { force: true };
+
+ // Prevent recursion
+ this._prevStatus = this.status;
+ this.status = status;
+
+ // Update data state at this level.
+ if (status === K.READY_NEW) {
+ this.set('dataState', 'create', setOptions);
+ } else if (status === K.READY_CLEAN) {
+ this.set('dataState', 'read', setOptions);
+ } else if (status === K.READY_DIRTY) {
+ this.set('dataState', 'update', setOptions);
+ } else if (status === K.DESTROYED_DIRTY) {
+ this.set('dataState', 'delete', setOptions);
+ }
+
+ this.trigger('statusChange', this, status, options);
+ //XT.log(this.recordType + ' id: ' + this.id +
+ // ' changed to ' + this.getStatusString());
+ return this;
+ },
+
+ /**
+ Sync to xTuple data source.
+ */
+ sync: function (method, model, options) {
+ options = options ? _.clone(options) : {};
+ if (!options.databaseType) { options.databaseType = this.databaseType; }
+ var that = this,
+ dataSource = options.dataSource || XT.dataSource,
+ id = options.id || model.id,
+ recordType = this.recordType,
+ result,
+ error = options.error;
+
+ options.error = function (resp) {
+ var K = XM.Model;
+ that.setStatus(K.ERROR);
+ if (error) { error(model, resp, options); }
+ };
+
+ // Read
+ if (method === 'read' && recordType && id && options.success) {
+ result = dataSource.retrieveRecord(recordType, id, options);
+
+ // Write
+ } else if (method === 'create' || method === 'update' || method === 'delete') {
+ result = dataSource.commitRecord(model, options);
+ }
+
+ return result || false;
+ }
+
+ });
+
+ XM.SimpleModel = XM.SimpleModel.extend({status: XM.Model.EMPTY});
+
+}());
View
2 node-datasource/database/source/xt/tables/oauth2client.sql
@@ -13,7 +13,7 @@ select xt.add_column('oauth2client','oauth2client_active', 'boolean', '', 'xt',
select xt.add_column('oauth2client','oauth2client_issued', 'timestamp', '', 'xt', 'The datetime that the client was registered');
select xt.add_column('oauth2client','oauth2client_auth_uri', 'text', '', 'xt', 'The Authorization Endpoint URI.');
select xt.add_column('oauth2client','oauth2client_token_uri', 'text', '', 'xt', 'The Token Endpoint URI.');
-select xt.add_column('oauth2client','oauth2client_redirect_uris', 'text', 'not null', 'xt', 'A list of valid Redirection Endpoint URIs.');
+select xt.add_column('oauth2client','oauth2client_redirect_uris', 'text[]', 'not null', 'xt', 'A list of valid Redirection Endpoint URIs.');
select xt.add_column('oauth2client','oauth2client_delegated_access', 'boolean', '', 'xt', 'Flag to allow "service_account" client to use delegated access as another user.');
select xt.add_column('oauth2client','oauth2client_client_x509_cert_url', '', 'text', 'xt', 'The URL of the public x509 certificate, used to verify JWTs signed by the client.');
select xt.add_column('oauth2client','oauth2client_auth_provider_x509_cert_url', 'text', '', 'xt', 'The URL of the public x509 certificate, used to verify the signature on JWTs, such as ID tokens, signed by the authentication provider.');
View
52 node-datasource/lib/ext/models.js
@@ -61,6 +61,34 @@ white:true*/
@extends XM.Model
*/
+ XM.Oauth2client = XM.SimpleModel.extend({
+ /** @scope XM.Organization.prototype */
+
+ recordType: 'XM.Oauth2client',
+
+ databaseType: 'global'
+
+ });
+
+ /**
+ @class
+
+ @extends XM.Model
+ */
+ XM.Oauth2token = XM.SimpleModel.extend({
+ /** @scope XM.Organization.prototype */
+
+ recordType: 'XM.Oauth2token',
+
+ databaseType: 'global'
+
+ });
+
+ /**
+ @class
+
+ @extends XM.Model
+ */
XM.Organization = XM.Model.extend({
/** @scope XM.Organization.prototype */
@@ -243,6 +271,30 @@ white:true*/
@extends XM.Collection
*/
+ XM.Oauth2clientCollection = XM.Collection.extend({
+ /** @scope XM.OrganizationCollection.prototype */
+
+ model: XM.Oauth2client
+
+ });
+
+ /**
+ @class
+
+ @extends XM.Collection
+ */
+ XM.Oauth2tokenCollection = XM.Collection.extend({
+ /** @scope XM.OrganizationCollection.prototype */
+
+ model: XM.Oauth2token
+
+ });
+
+ /**
+ @class
+
+ @extends XM.Collection
+ */
XM.OrganizationCollection = XM.Collection.extend({
/** @scope XM.OrganizationCollection.prototype */
View
66 node-datasource/oauth2/db/accesstokens.js
@@ -1,12 +1,64 @@
-var tokens = {};
+exports.findByAccessToken = function(key, done) {
+ "use strict";
+ var code = new XM.Oauth2tokenCollection(),
+ options = {};
-exports.find = function(key, done) {
- var token = tokens[key];
- return done(null, token);
+ options.success = function (res) {
+ if (res.models.length !== 1) {
+ var message = "Error fetching OAuth 2.0 access token.";
+ X.log(message);
+ return done(null, null);
+ }
+
+ return done(null, res.models[0]);
+ };
+
+ options.error = function (res, err) {
+ if (err.code === 'xt1007') {
+ // XXX should "result not found" really be an error?
+ return done(null, null);
+ } else {
+ var message = "Error validating OAuth 2.0 access token.";
+ X.log(message);
+ return done(new Error(message));
+ }
+ };
+
+ options.query = {};
+ options.query.parameters = [{attribute: "accessToken", value: key}];
+
+ code.fetch(options);
};
-exports.save = function(token, userID, clientID, done) {
- tokens[token] = { userID: userID, clientID: clientID };
- return done(null);
+exports.findByRefreshToken = function(key, done) {
+ "use strict";
+ var code = new XM.Oauth2tokenCollection(),
+ options = {};
+
+ options.success = function (res) {
+ if (res.models.length !== 1) {
+ var message = "Error fetching OAuth 2.0 refresh token.";
+ X.log(message);
+ return done(null, null);
+ }
+
+ return done(null, res.models[0]);
+ };
+
+ options.error = function (res, err) {
+ if (err.code === 'xt1007') {
+ // XXX should "result not found" really be an error?
+ return done(null, null);
+ } else {
+ var message = "Error validating OAuth 2.0 refresh token.";
+ X.log(message);
+ return done(new Error(message));
+ }
+ };
+
+ options.query = {};
+ options.query.parameters = [{attribute: "refreshToken", value: key}];
+
+ code.fetch(options);
};
View
98 node-datasource/oauth2/db/authorizationcodes.js
@@ -1,12 +1,96 @@
-var codes = {};
+/*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
+regexp:true, undef:true, strict:true, trailing:true, white:true */
+/*global X:true, XM:true, console:true*/
+/**
+ * Find an issued auth code in the database.
+ *
+ * @param {string} Auth code send from a client.
+ * @param {Function} Function to call the move along.
+ */
+exports.find = function (code, done) {
+ "use strict";
-exports.find = function(key, done) {
- var code = codes[key];
- return done(null, code);
+ var authCode = new XM.Oauth2tokenCollection(),
+ options = {};
+
+ options.success = function (res) {
+ // We should only get one record back matching the authCode.
+ if (res.models.length !== 1) {
+ var message = "Error fetching OAuth 2.0 authenticating code.";
+ X.log(message);
+
+ // No match or multiple which is not allowed. Send nothing.
+ return done(null, null);
+ }
+
+ // Send that XM.Oauth2token model along.
+ return done(null, res.models[0]);
+ };
+
+ options.error = function (res, err) {
+ if (err.code === 'xt1007') {
+ // XXX should "result not found" really be an error?
+ return done(null, null);
+ } else {
+ var message = "Error authenticating OAuth 2.0 authenticating code.";
+ X.log(message);
+ return done(new Error(message));
+ }
+ };
+
+ // Fetch the collection looking for a matching authCode.
+ options.query = {};
+ options.query.parameters = [{attribute: "authCode", value: code}];
+ authCode.fetch(options);
};
-exports.save = function(code, clientID, redirectURI, userID, done) {
- codes[code] = { clientID: clientID, redirectURI: redirectURI, userID: userID };
- return done(null);
+/**
+ * Save an auth code to the database.
+ *
+ * @param {string} Auth code send from a client.
+ * @param {string} OAuth 2.0 client ID.
+ * @param {string} Redirect URI to reply to the client at.
+ * @param {string} User id/name.
+ * @param {string} Scope/org the auth code and tokens will be valid for.
+ * @param {Function} Function to call the move along.
+ */
+exports.save = function (code, clientID, redirectURI, userID, scope, done) {
+ "use strict";
+
+ var authCode = new XM.Oauth2token(),
+ saveOptions = {},
+ initCallback = function (model, value) {
+ if (model.id) {
+ // Now that model is ready, set attributes and save.
+ var codeAttributes = {
+ user: userID,
+ clientID: clientID,
+ redirectURI: redirectURI,
+ scope: scope,
+ state: "Auth Code Issued",
+ authCode: code,
+ authCodeIssued: new Date(),
+ tokenType: "bearer"
+ };
+
+ // Try to save auth code data to the database.
+ model.save(codeAttributes, saveOptions);
+ } else {
+ return done && done(new Error('Cannot save authenticating code. No id set.'));
+ }
+ };
+
+ saveOptions.success = function (model) {
+ return done(null);
+ };
+ saveOptions.error = function (model, err) {
+ return done && done(err);
+ };
+
+ // Register on change of id callback to know when the model is initialized.
+ authCode.on('change:id', initCallback);
+
+ // Initialize the model.
+ authCode.initialize(null, {isNew: true});
};
View
95 node-datasource/oauth2/db/clients.js
@@ -1,28 +1,89 @@
+/*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
+regexp:true, undef:true, strict:true, trailing:true, white:true */
+/*global X:true, XM:true, console:true*/
+
// TODO - Need to store and check against:
// -- approved callback URLs
// -- client type, e.g. "installed applicaion", "web server", "service account"
// OAuth 2.0 server should respond differently based on the cleint type.
-var clients = [
- { id: '1', name: 'xTuple', clientId: '766398752140.apps.googleusercontent.com', clientSecret: 'sXZdl3_RJgykttfoT_BOyJuK' }
-];
+/**
+ * Find an matching client by id in the database.
+ *
+ * @param {string} Database client id field.
+ * @param {Function} Function to call the move along.
+ */
+exports.find = function (id, done) {
+ "use strict";
+
+ var client = new XM.Oauth2clientCollection(),
+ options = {};
+
+ options.success = function (res) {
+ if (res.models.length !== 1) {
+ var message = "Error fetching OAuth 2.0 client.";
+ X.log(message);
+ return done(null, null);
+ }
+
+ return done(null, res.models[0]);
+ };
-exports.find = function(id, done) {
- for (var i = 0, len = clients.length; i < len; i++) {
- var client = clients[i];
- if (client.id === id) {
- return done(null, client);
+ options.error = function (res, err) {
+ if (err.code === 'xt1007') {
+ // XXX should "result not found" really be an error?
+ return done(null, null);
+ } else {
+ var message = "Error validating OAuth 2.0 client.";
+ X.log("Error validating OAuth 2.0 client.");
+ return done(new Error(message));
}
- }
- return done(null, null);
+ };
+
+ options.query = {};
+ options.query.parameters = [{attribute: "id", value: id}];
+
+ client.fetch(options);
};
-exports.findByClientId = function(clientId, done) {
- for (var i = 0, len = clients.length; i < len; i++) {
- var client = clients[i];
- if (client.clientId === clientId) {
- return done(null, client);
+/**
+ * Find an matching client by clientID in the database.
+ *
+ * @param {string} OAuth 2.0 client ID.
+ * @param {Function} Function to call the move along.
+ */
+exports.findByClientId = function (clientID, done) {
+ "use strict";
+
+ var client = new XM.Oauth2clientCollection(),
+ options = {};
+
+ options.success = function (res) {
+ if (res.models.length !== 1) {
+ var message = "Error fetching OAuth 2.0 client.";
+ X.log(message);
+ return done(null, null);
+ }
+
+ return done(null, res.models[0]);
+ };
+
+ options.error = function (res, err) {
+ if (err.code === 'xt1007') {
+ // XXX should "result not found" really be an error?
+ return done(null, null);
+ } else {
+ var message = "Error validating OAuth 2.0 client.";
+ X.log("Error validating OAuth 2.0 client.");
+ return done(new Error(message));
}
- }
- return done(null, null);
+ };
+
+ options.query = {};
+ options.query.parameters = [{attribute: "clientID", value: clientID}];
+
+ client.fetch(options);
};
+
+// TODO - Need an admin iterface that can add new clients.
+// TODO - Need a save/destroy function for that admin interface to call.
View
159 node-datasource/oauth2/oauth2.js
@@ -50,10 +50,17 @@ server.deserializeClient(function(id, done) {
// values, and will be exchanged for an access token.
server.grant(oauth2orize.grant.code(function (client, redirectURI, user, ares, done) {
- var code = utils.uid(16)
+ if (!client || !user || !redirectURI || !ares) { return done(null, false); }
+
+ // Generate the auth code.
+ var code = utils.uid(16);
+
+ // Save auth data to the database.
+ db.authorizationCodes.save(code, client.get("clientID"), redirectURI, user.id, ares.scope, function (err) {
+ if (err) {
+ return done(err);
+ }
- db.authorizationCodes.save(code, client.id, redirectURI, user.id, function (err) {
- if (err) { return done(err); }
done(null, code);
});
}));
@@ -67,75 +74,45 @@ server.grant(oauth2orize.grant.code(function (client, redirectURI, user, ares, d
server.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, done) {
db.authorizationCodes.find(code, function (err, authCode) {
if (err) { return done(err); }
- if (client.id !== authCode.clientID) { return done(null, false); }
- if (redirectURI !== authCode.redirectURI) { return done(null, false); }
+ if (!authCode || !client) { return done(null, false); }
+ if (client.get("clientID") !== authCode.get("clientID")) { return done(null, false); }
+ if (redirectURI !== authCode.get("redirectURI")) { return done(null, false); }
// Create the tokens.
var accessToken = utils.uid(256),
refreshToken = utils.uid(256),
- params = {};
-
- params.token_type = 'bearer';
- params.expires_in = new Date(today.getTime() + (24 * 60 * 60 * 1000));
-
- // Save the accessToken.
- // TODO - Save params.expires_in and use that to validate tokens.
- db.accessTokens.save(accessToken, authCode.userID, authCode.clientID, function (err) {
- if (err) {
- return done(err);
- }
-
- // TODO - Need a refreshToken store.
- // It should have:
- // - user_id,
- // - client_id, - Client sending Oauth requests.
- // - redirect_uri, - Determines where the response is sent.
- // - scope, - List of scopes that access was granted for for this token. Just one org and maybe the userinfo scope.
- // - state, - Indicates any state which may be useful to your application upon receipt of the response.
- // - approval_prompt, - Indicates if the user should be re-prompted for consent.
- // - auth_code, - Removed after first exchange for...
- // - auth_code_issued, - Datetime auth code was issued. If more than 30 minutes have passed without exchange, remove whole row.
- // - auth_code_expires_in, - GMT datetime when this code expires.
- // - refresh_token, - Does not expire
- // - refresh_token_issued, - Datetime refresh token was created
- // - refresh_expires_in, - GMT datetime when this token expires.
- // - access_token, - Current issued/valid access token.
- // - access_token_issued, - GMT datetime when this token was issued.
- // - access_expires_in, - GMT datetime when this token expires.
- // - token_type, - Bearer for now, MAC later
- // - access_type, - online or offline
- // - delegate, - user_id for which the application is requesting delegated access as.
-
- // TODO - Need client store.
- // It should have:
- // - client_id, - Generated id
- // - client_secret, - Generated random key.
- // - client_name, - Name of app requesting permission.
- // - client_email, - Contact email for the client app.
- // - client_web_site, - URL for the client app.
- // - client_logo, - logo image for the client app.
- // - client_type, - "web_server", "installed_app", "service_account"
- // - active, - Boolean to deactivate a client.
- // - issued, - Datetime client was registered.
- // - auth_uri,
- // - token_uri,
- // - redirect_uris, - URIs registered for this client to redirect to.
- // - delegated_access, - Boolean if "service_account" can act on behalf of other users
- // - client_x509_cert_url, - Public key cert for "service_account"
- // - auth_provider_x509_cert_url
-
- // Save the refreshToken.
- db.accessTokens.save(refreshToken, authCode.userID, client.id, function (err) {
- if (err) {
- return done(err);
- }
-
- // TODO - Now that the auth code has been exchanged, we need to delete it.
-
- // Send the tokens along.
- done(null, accessToken, refreshToken, params);
- });
- });
+ saveOptions = {},
+ today = new Date(),
+ expires = new Date(today.getTime() + (24 * 60 * 60 * 1000)),
+ tokenAttributes = {},
+ tokenType = 'bearer';
+
+ saveOptions.success = function (model) {
+ var params = {};
+
+ params.token_type = model.get("tokenType");
+ params.expires_in = new Date() - expires;
+
+ // Send the tokens along.
+ return done(null, model.get("accessToken"), model.get("refreshToken"), params);
+ };
+ saveOptions.error = function (model, err) {
+ return done && done(err);
+ };
+
+ // Set model values and save.
+ authCode.set("state", "Token Issued");
+ authCode.set("authCode", null);
+ authCode.set("authCodeExpires", new Date());
+ authCode.set("refreshToken", refreshToken);
+ authCode.set("refreshIssued", new Date());
+ authCode.set("accessToken", accessToken);
+ authCode.set("accessIssued", new Date());
+ authCode.set("accessExpires", expires);
+ authCode.set("tokenType", tokenType);
+ authCode.set("accessType", "offline"); // Default for now...
+
+ authCode.save(null, saveOptions);
});
}));
@@ -172,11 +149,24 @@ exports.authorization = [
server.authorization(function(clientID, redirectURI, scope, type, done) {
db.clients.findByClientId(clientID, function(err, client) {
if (err) { return done(err); }
- // WARNING: For security purposes, it is highly advisable to check that
- // redirectURI provided by the client matches one registered with
- // the server. For simplicity, this example does not. You have
- // been warned.
- return done(null, client, redirectURI);
+ if (!client) { return done(null, false); }
+
+ var matches = false;
+
+ // For security purposes, we check that redirectURI provided
+ // by the client matches one registered with the server.
+ _.each(client.get("redirectURIs"), function (value, key, list) {
+ // Check if the requested redirectURI is in approved client.redirectURIs.
+ if (value === redirectURI) {
+ matches = true;
+ }
+ });
+
+ if (matches) {
+ return done(null, client, redirectURI);
+ } else {
+ return done(null, false);
+ }
});
}),
function(req, res, next){
@@ -192,8 +182,15 @@ exports.authorization = [
// the URI to call to get a user's scope/org list: 'https://mobile.xtuple.com/auth/userinfo.xxx'
},
login.ensureLoggedIn({redirectTo: "/"}),
- function(req, res){
- res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client });
+ function(req, res, next){
+ var scope;
+
+ if (req.session && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
+ scope = req.session.passport.user.organization;
+ res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user.id, client: req.oauth2.client.get("clientName"), scope: scope });
+ } else {
+ next(new Error('Invalid OAuth 2.0 scope.'));
+ }
}
]
@@ -211,7 +208,17 @@ exports.authorization = [
exports.decision = [
login.ensureLoggedIn({redirectTo: "/"}),
- server.decision()
+ server.decision(function(req, next){
+ // Add the approved scope/org to req.oauth2.res.
+ var ares = {};
+
+ if (req.session && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
+ ares.scope = req.session.passport.user.organization;
+ return next(null, ares);
+ } else {
+ return next(new Error('Invalid OAuth 2.0 scope.'));
+ }
+ })
]
View
8 node-datasource/oauth2/passport.js
@@ -88,7 +88,7 @@ passport.use(new BasicStrategy(
db.clients.findByClientId(username, function (err, client) {
if (err) { return done(err); }
if (!client) { return done(null, false); }
- if (client.clientSecret !== password) { return done(null, false); }
+ if (client.get("clientSecret") !== password) { return done(null, false); }
return done(null, client);
});
}
@@ -100,7 +100,7 @@ passport.use(new ClientPasswordStrategy(
db.clients.findByClientId(clientId, function (err, client) {
if (err) { return done(err); }
if (!client) { return done(null, false); }
- if (client.clientSecret !== clientSecret) { return done(null, false); }
+ if (client.get("clientSecret") !== clientSecret) { return done(null, false); }
return done(null, client);
});
}
@@ -117,11 +117,11 @@ passport.use(new ClientPasswordStrategy(
passport.use(new BearerStrategy(
function (accessToken, done) {
"use strict";
- db.accessTokens.find(accessToken, function (err, token) {
+ db.accessTokens.findByAccessToken(accessToken, function (err, token) {
if (err) { return done(err); }
if (!token) { return done(null, false); }
- db.users.findByUsername(token.userID, function (err, user) {
+ db.users.findByUsername(token.get("user"), function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
// to keep this example simple, restricted scopes are not implemented,
View
4 node-datasource/views/dialog.ejs
@@ -1,5 +1,5 @@
-<p>Hi <%= user.name %>!</p>
-<p><b><%= client.name %></b> is requesting access to your account.</p>
+<p>Hi <%= user %>!</p>
+<p><b><%= client %></b> is requesting access to your account on the <b><%= scope %></b> database.</p>
<p>Do you approve?</p>
<form action="/dialog/authorize/decision" method="post">

0 comments on commit d59e26f

Please sign in to comment.