Skip to content
This repository has been archived by the owner on Nov 16, 2018. It is now read-only.

Commit

Permalink
Merge pull request #51 from acquia/oauth-automatic
Browse files Browse the repository at this point in the history
Automatically fetch OAuth Bearer tokens.
  • Loading branch information
mattgrill authored Jan 16, 2017
2 parents f376f86 + 07c10ad commit 604a488
Show file tree
Hide file tree
Showing 16 changed files with 663 additions and 371 deletions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ npm run build

Ensure that you have set up cross-origin resource sharing on your Drupal site to enable Waterwheel to perform necessary tasks. Instructions for [Apache](http://enable-cors.org/server_apache.html) or [Nginx](http://enable-cors.org/server_nginx.html).

The majority of the functionality in Waterwheel is dependent on the [Waterwheel-Drupal module](https://www.drupal.org/project/waterwheel). Please install and enable this module in your Drupal site before attempting to use the functionality offered in this library.
Some functionality in Waterwheel.js is dependent on the [Waterwheel-Drupal module](https://www.drupal.org/project/waterwheel). Please install and enable this module in your Drupal site before attempting to use the functionality offered in this library.

## Documentation

Expand All @@ -41,20 +41,52 @@ The majority of the functionality in Waterwheel is dependent on the [Waterwheel-
```javascript
// Server
const Waterwheel = require('waterwheel');
const waterwheel = new Waterwheel({base: 'http://test.dev', credentials: {oauth: '12345'}});
const waterwheel = new Waterwheel({
base: 'http://drupal.localhost',
oauth: {
grant_type: 'GRANT-TYPE',
client_id: 'CLIENT-ID',
client_secret: 'CLIENT-SECRET',
username: 'USERNAME',
password: 'PASSWORD',
scope: 'SCOPE'
}
});

// Browser
import '../../path/to/node_modules/waterwheel/dist/waterwheel.js'
const waterwheel = new window.Waterwheel({base: 'http://test.dev', credentials: {oauth: '12345'}});
const waterwheel = new Waterwheel({
base: 'http://drupal.localhost',
oauth: {
grant_type: 'GRANT-TYPE',
client_id: 'CLIENT-ID',
client_secret: 'CLIENT-SECRET',
username: 'USERNAME',
password: 'PASSWORD',
scope: 'SCOPE'
}
});

// With resources
const waterwheel = new Waterwheel({base: 'http://test.dev', credentials: {oauth: '12345'}, resources: require('./resources.json')});
const resources = require('./resources.json');
const waterwheel = new Waterwheel({
base: 'http://drupal.localhost',
resources: resources,
oauth: {
grant_type: 'GRANT-TYPE',
client_id: 'CLIENT-ID',
client_secret: 'CLIENT-SECRET',
username: 'USERNAME',
password: 'PASSWORD',
scope: 'SCOPE'
}
});
```

Waterwheel when instantiated accepts a single object,
- `base`: The base path for your Drupal instance. All request paths will be built from this base
- `credentials`: An object containing the OAuth Bearer token used to authenticate with Drupal. Currently Waterwheel requires a token for all requests. The [Simple OAuth](https://www.drupal.org/project/simple_oauth) module is recommended for this.
- `base`: The base path for your Drupal instance. All request paths will be built from this base.
- `resources`: A JSON object that represents the resources available to `waterwheel`.
- `oauth`: An object containing information required for fetching and refreshing OAuth Bearer tokens. _Currently Waterwheel requires a token for all requests._ The [Simple OAuth](https://www.drupal.org/project/simple_oauth) module is recommended for this.
- `timeout`: How long an HTTP request should idle for before being canceled.
- `jsonapiPrefix`: If you have overridden the JSON API prefix, specify it here and Waterwheel will use this over the default of `jsonapi`.

Expand Down
29 changes: 13 additions & 16 deletions lib/entity.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
const Request = require('./helpers/request');
const methods = require('./helpers/methods');

module.exports = class Entity extends Request {
module.exports = class Entity {
/**
* Construct a new Entity
* @param {object} options
* The configuration used to create this entity.
* @param {string} options.base
* The base URL
* @param {object} options.credentials
* The request credentials.
* @param {string} options.credentials.oauth
* The OAuth2 Bearer token
* @param {object} options.methods
* The paths representing each CRUD action.
* @param {string} options.methods.GET
Expand All @@ -36,10 +31,12 @@ module.exports = class Entity extends Request {
* An array of field names required when creating an entity.
* @param {object} options.metadata.properties
* An object describing all the fields on a bundle and their properties.
* @param {object} request
* A shared requestor class instance.
*/
constructor(options) {
// Call the parents constructor.
super(options);
constructor(options, request) {
this.options = options;
this.request = request;
}

/**
Expand All @@ -55,7 +52,7 @@ module.exports = class Entity extends Request {
get(identifier, format = 'json') {
return !this.options.methods.hasOwnProperty(methods.get) ?
Promise.reject(`The method, ${methods.get}, is not available.`) :
this.issueRequest(methods.get, `${this.options.methods.GET.path.replace(this.options.methods.GET.path.match(/\{.*?\}/), identifier)}?_format=${format}`, '');
this.request.issueRequest(methods.get, `${this.options.methods.GET.path.replace(this.options.methods.GET.path.match(/\{.*?\}/), identifier)}?_format=${format}`, '');
}

/**
Expand All @@ -71,8 +68,8 @@ module.exports = class Entity extends Request {
patch(identifier, body = {}, format = 'application/json') {
return !this.options.methods.hasOwnProperty(methods.patch) ?
Promise.reject(`The method, ${methods.patch}, is not available.`) :
this.getXCSRFToken()
.then((csrfToken) => this.issueRequest(methods.patch, `${this.options.methods.PATCH.path.replace(this.options.methods.PATCH.path.match(/\{.*?\}/), identifier)}`, csrfToken, {'Content-Type': format}, body));
this.request.getXCSRFToken()
.then((csrfToken) => this.request.issueRequest(methods.patch, `${this.options.methods.PATCH.path.replace(this.options.methods.PATCH.path.match(/\{.*?\}/), identifier)}`, csrfToken, {'Content-Type': format}, body));
}

/**
Expand All @@ -98,8 +95,8 @@ module.exports = class Entity extends Request {
}
}

return this.getXCSRFToken()
.then((csrfToken) => this.issueRequest(methods.post, this.options.methods.POST.path, csrfToken, {'Content-Type': format}, body));
return this.request.getXCSRFToken()
.then((csrfToken) => this.request.issueRequest(methods.post, this.options.methods.POST.path, csrfToken, {'Content-Type': format}, body));
}

/**
Expand All @@ -112,8 +109,8 @@ module.exports = class Entity extends Request {
delete(identifier) {
return !this.options.methods.hasOwnProperty(methods.delete) ?
Promise.reject(`The method, ${methods.delete}, is not available.`) :
this.getXCSRFToken()
.then((csrfToken) => this.issueRequest(methods.delete, `${this.options.methods.DELETE.path.replace(this.options.methods.DELETE.path.match(/\{.*?\}/), identifier)}`, csrfToken));
this.request.getXCSRFToken()
.then((csrfToken) => this.request.issueRequest(methods.delete, `${this.options.methods.DELETE.path.replace(this.options.methods.DELETE.path.match(/\{.*?\}/), identifier)}`, csrfToken));
}

/**
Expand Down
50 changes: 0 additions & 50 deletions lib/helpers/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,12 @@ module.exports = class Base {
* The configuration used to create a new instance of Waterwheel.
* @param {string} options.base
* The base URL.
* @param {object} options.credentials
* The credentials used with each request.
* @param {string} options.credentials.oauth
* The OAuth2 Bearer token.
* @param {object} options.resources
* A JSON object representing all the resources available to Waterwheel.
* @param {string} options.timeout
* How long AXIOS should wait before bailing on a request.
*/
constructor(options) {
this.options = Object.assign({
timeout: 500
}, options);
this.setCredentials(this.options.credentials);
}
/**
* Ensure that the type of credentials being set are ones that Waterwheel supports.
* @param {object} credentials
* Object containing credentials. Currently just Oauth.
* @return {boolean}
* If the credentials are valid, return true.
*/
testCredentials(credentials) {
const possibleAuth = ['oauth'];
if (!(Object.keys(credentials).filter(key => possibleAuth.indexOf(key) !== -1)).length) {
return false;
}
return true;
}

/**
* Set the current credentials.
* @param {object} credentials
* The credentials object.
* @param {object} credentials.user
* The Drupal user making the request.
* @param {object} credentials.pass
* The password for the above user.
*/
setCredentials(credentials) {
if (this.testCredentials(credentials)) {
this.options.credentials = credentials;
return;
}
throw new Error('Incorrect authentication method.');
}

/**
* Get the current credentials object.
* @return {object}
* The current credentials, .user and .pass.
*/
getCredentials() {
return this.options.credentials;
}

/**
* Set the base url.
* @param {string} base
Expand Down
53 changes: 53 additions & 0 deletions lib/helpers/oauth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const axios = require('axios');
const qs = require('qs');

module.exports = class OAuth {
constructor(basePath, OAuthOptions) {
this.basePath = basePath;
this.tokenInformation = Object.assign({}, OAuthOptions);
this.tokenInformation.grant_type = 'password'; // eslint-disable-line camelcase
}
/**
* Get an OAuth Token.
* @return {promise}
* The resolved promise of fetching the oauth token.
*/
getToken() {
const currentTime = new Date().getTime();
// Resolve if token already exists and is fresh
if (this.tokenInformation.access_token && (
this.hasOwnProperty('tokenExpireTime') &&
this.tokenExpireTime > currentTime
)) {
return Promise.resolve();
}
// If token is already being fetched, use that one.
else if (this.bearerPromise) {
return this.bearerPromise;
}
// If token has already been fetched switch grant_type to refresh_token.
else if (this.tokenInformation.access_token) {
this.tokenInformation.grant_type = 'refresh_token'; // eslint-disable-line camelcase
}

this.bearerPromise = axios({
method: 'post',
url: `${this.basePath}/oauth/token`,
data: qs.stringify(this.tokenInformation)
})
.then(response => {
delete this.bearerPromise;
let t = new Date();
t.setSeconds(+t.getSeconds() + response.data.expires_in);
this.tokenExpireTime = t.getTime();
Object.assign(this.tokenInformation, response.data);
return response.data;
})
.catch(e => {
delete this.bearerPromise;
return Promise.reject(e);
});

return this.bearerPromise;
}
};
26 changes: 10 additions & 16 deletions lib/helpers/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,12 @@ module.exports = class Request extends Base {
* The configuration used to create a new instance of Waterwheel.
* @param {string} options.base
* The base URL.
* @param {object} options.credentials
* The credentials used with each request.
* @param {string} options.credentials.oauth
* The OAuth2 Bearer token.
* @param {object} options.resources
* A JSON object representing all the resources available to Waterwheel.
* @param {string} options.timeout
* How long AXIOS should wait before bailing on a request.
* @param {object} oauth
* The OAuth options.
*/
constructor(options) {
constructor(options, oauth) {
super(options);
this.oauth = oauth;
this.axios = require('axios');
}

Expand All @@ -40,7 +35,8 @@ module.exports = class Request extends Base {
* A Promise that when fulfilled returns a response from the request.
*/
issueRequest(method, url, XCSRFToken, additionalHeaders, body, baseOverride) {
return new Promise((resolve, reject) => {
return this.oauth.getToken()
.then(() => {
const options = {
method: method,
timeout: this.options.timeout,
Expand All @@ -50,9 +46,7 @@ module.exports = class Request extends Base {
}
};

if (this.options.credentials.oauth) {
options.headers.Authorization = `Bearer ${this.options.credentials.oauth}`;
}
options.headers.Authorization = `Bearer ${this.oauth.tokenInformation.access_token}`;

// If this is a GET request,
// or we didn't pass a token drop the X-CSRF-Token header.
Expand All @@ -72,8 +66,8 @@ module.exports = class Request extends Base {
options.data = body;
}

this.axios(options)
.then(res => resolve(res.data))
return this.axios(options)
.then(res => Promise.resolve(res.data))
.catch(err => {
const error = new Error();
if (err.message && err.message.indexOf('timeout') !== -1) {
Expand All @@ -85,7 +79,7 @@ module.exports = class Request extends Base {
error.status = err.response ? err.response.status : 500;
}

return reject(error);
return Promise.reject(error);
});
});
}
Expand Down
13 changes: 6 additions & 7 deletions lib/jsonapi.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
const Request = require('./helpers/request');
const methods = require('./helpers/methods');

const qs = require('qs');

module.exports = class JSONAPI extends Request {
constructor(options) {
super(options);
module.exports = class JSONAPI {
constructor(options, request) {
this.request = request;
this.jsonapiPrefix = options.jsonapiPrefix || 'jsonapi';
}

Expand All @@ -24,7 +23,7 @@ module.exports = class JSONAPI extends Request {
get(resource, params, id = false) {
const format = 'api_json';
const url = `/${this.jsonapiPrefix}/${resource}${id ? `/${id}` : ''}?_format=${format}${Object.keys(params).length ? `&${qs.stringify(params, {indices: false})}` : ''}`;
return this.issueRequest(methods.get, url, '');
return this.request.issueRequest(methods.get, url);
}

/**
Expand All @@ -39,7 +38,7 @@ module.exports = class JSONAPI extends Request {
*/
post(resource, body) {
const format = 'api_json';
return this.issueRequest(
return this.request.issueRequest(
methods.post,
`/${this.jsonapiPrefix}/${resource}?_format=${format}`,
'',
Expand All @@ -62,7 +61,7 @@ module.exports = class JSONAPI extends Request {
*/
patch(resource, body) {
const format = 'api_json';
return this.issueRequest(
return this.request.issueRequest(
methods.patch,
`/${this.jsonapiPrefix}/${resource}?_format=${format}`,
'',
Expand Down
Loading

0 comments on commit 604a488

Please sign in to comment.