├── .gitignore ├── README.md ├── index.js ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress REST API OAuth 1 Client 2 | 3 | JavaScript OAuth 1 Client for the WordPress REST API v2. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install --save wordpress-rest-api-oauth-1 9 | ``` 10 | 11 | ## Configuration 12 | 13 | ### Without Authentication 14 | 15 | ```JS 16 | import api from 'wordpress-rest-api-oauth-1' 17 | 18 | const demoApi = new api({ 19 | url: 'https://demo.wp-api.org/' 20 | }) 21 | ``` 22 | ### Using OAuth 1 Directly 23 | 24 | To communication and authenticate using OAuth 1 with your WordPress site directly: 25 | 26 | ```JS 27 | import api from 'wordpress-rest-api-oauth-1' 28 | 29 | const demoApi = new api({ 30 | url: 'https://demo.wp-api.org/', 31 | credentials: { 32 | client: { 33 | public: 'xxxxxx', 34 | secret: 'xxxxxxxxxxxxxxxxxxx' 35 | } 36 | } 37 | }) 38 | ``` 39 | 40 | ### Using the WordPress Authentication Broker 41 | 42 | To establish a connection to a WordPress site that accepts the [WordPress REST API Broker](https://apps.wp-api.org/): 43 | 44 | ```JS 45 | import api from 'wordpress-rest-api-oauth-1' 46 | 47 | const demoApi = new api({ 48 | url: 'https://demo.wp-api.org/', 49 | brokerCredentials: { 50 | client: { 51 | public: 'xxxxxx', 52 | secret: 'xxxxxxxxxxxxxxxxxxx' 53 | } 54 | } 55 | }) 56 | 57 | // Get OAuth client tokens for the specified site. This is not needed if using `authorize()`. 58 | demoApi.getConsumerToken().then( token => { 59 | console.log( token ) 60 | }) 61 | ``` 62 | 63 | ### Usage 64 | 65 | #### Authorize / OAuth Flow 66 | 67 | There is two ways to get authentication tokens, one "high level" function, or you can implement your own flow using the underlaying function. 68 | 69 | ##### The Quick Way 70 | 71 | ```JS 72 | demoApi.authorize().then( function() { 73 | console.log( 'All API requests are now authenticated.' ) 74 | }) 75 | 76 | // Note: the above will cause a redirect / resume of the app in the event that the user needs to authorize. 77 | ``` 78 | 79 | ##### Control your own flow 80 | 81 | ```JS 82 | // Get client tokens from the broker (optional) 83 | demoApi.getConsumerToken().then( ... ) 84 | 85 | // Get a request token 86 | demo.getRequestToken() ) 87 | .then( token => { 88 | // handle the user authorize redirect with token.redirectURL 89 | }) 90 | 91 | // Exchange for an access token 92 | demo.getAccessToken( oAuthVerifier ) 93 | .then( token => { 94 | // save the token to localStorage etc. 95 | }) 96 | ``` 97 | 98 | #### Make API Requests 99 | 100 | You can make API requests directly with this library for both authenticated requests and anonymous. 101 | 102 | 103 | ```JS 104 | demoApi.get( '/wp/v2/posts', { per_page: 5 } ).then( posts => { 105 | console.log( posts ) 106 | }) 107 | 108 | demoApi.post( '/wp/v2/posts', { title: 'Test new post' } } ).then( post => { 109 | console.log( post ) 110 | }) 111 | 112 | demoApi.del( '/wp/v2/posts/1' ).then( post => { 113 | console.log( 'Deleted post.' ) 114 | }) 115 | ``` 116 | 117 | ### Loading and Saving Credentials 118 | 119 | With OAuth in the browser, you don't typically want to run through the authorization flow on every page load, so you can export and import the credentials if you wish: 120 | 121 | ```JS 122 | // init API with credentials: 123 | new api({ 124 | url: siteURL, 125 | credentials: JSON.parse( localStorage.getItem( 'authCredentials' ) ) 126 | }) 127 | 128 | // save the credentials 129 | localStorage.setItem( 'authCredentials', JSON.stringify( demoApi.config.credentials ) ) 130 | ``` 131 | 132 | You can also have the library store and retrieve the credentials: 133 | 134 | ```JS 135 | demoApi.restoreCredentials().get( '/wp/v2/users/me' ) 136 | 137 | demoApi.saveCredentials() // Save the credentials to localStorage 138 | ``` 139 | 140 | To implement restoring of credentials and auth in one go: 141 | 142 | ```JS 143 | demoApi.restoreCredentials().authorize().then( function() { 144 | demoApi.saveCredentials() 145 | }) 146 | ``` 147 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 10 | 11 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 12 | 13 | var _qs = require('qs'); 14 | 15 | var _qs2 = _interopRequireDefault(_qs); 16 | 17 | var _oauth = require('oauth-1.0a'); 18 | 19 | var _oauth2 = _interopRequireDefault(_oauth); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | var _class = function () { 26 | function _class(config) { 27 | _classCallCheck(this, _class); 28 | 29 | this.url = config.rest_url ? config.rest_url : config.url + 'wp-json'; 30 | this.url = this.url.replace(/\/$/, ''); 31 | this.credentials = config.credentials; 32 | 33 | if (this.credentials) { 34 | this.oauth = new _oauth2.default({ 35 | consumer: config.credentials.client, 36 | signature_method: 'HMAC-SHA1' 37 | }); 38 | } 39 | this.config = config; 40 | } 41 | 42 | _createClass(_class, [{ 43 | key: 'getConsumerToken', 44 | value: function getConsumerToken() { 45 | var _this = this; 46 | 47 | if (!this.config.brokerCredentials) { 48 | throw new Error('Config does not include a brokerCredentials value.'); 49 | } 50 | 51 | this.config.credentials.client = this.config.brokerCredentials.client; 52 | return this.post(this.config.brokerURL + 'broker/connect', { 53 | server_url: this.config.url 54 | }).then(function (data) { 55 | 56 | if (data.status && data.status === 'error') { 57 | throw { message: 'Broker error: ' + data.message, code: data.type }; 58 | } 59 | _this.config.credentials.client = { 60 | public: data.client_token, 61 | secret: data.client_secret 62 | }; 63 | 64 | return data; 65 | }); 66 | } 67 | }, { 68 | key: 'getRequestToken', 69 | value: function getRequestToken() { 70 | var _this2 = this; 71 | 72 | if (!this.config.callbackURL) { 73 | throw new Error('Config does not include a callbackURL value.'); 74 | } 75 | return this.post(this.config.url + 'oauth1/request', { 76 | callback_url: this.config.callbackURL 77 | }).then(function (data) { 78 | var redirectURL = _this2.config.url + 'oauth1/authorize?' + _qs2.default.stringify({ 79 | oauth_token: data.oauth_token, 80 | oauth_callback: _this2.config.callbackURL 81 | }); 82 | 83 | _this2.config.credentials.token = { 84 | secret: data.oauth_token_secret 85 | }; 86 | 87 | return { redirectURL: redirectURL, token: data }; 88 | }); 89 | } 90 | }, { 91 | key: 'getAccessToken', 92 | value: function getAccessToken(oauthVerifier) { 93 | var _this3 = this; 94 | 95 | return this.post(this.config.url + 'oauth1/access', { 96 | oauth_verifier: oauthVerifier 97 | }).then(function (data) { 98 | _this3.config.credentials.token = { 99 | public: data.oauth_token, 100 | secret: data.oauth_token_secret 101 | }; 102 | 103 | return _this3.config.credentials.token; 104 | }); 105 | } 106 | }, { 107 | key: 'authorize', 108 | value: function authorize(next) { 109 | 110 | var args = {}; 111 | var savedCredentials = window.localStorage.getItem('requestTokenCredentials'); 112 | if (window.location.href.indexOf('?')) { 113 | args = _qs2.default.parse(window.location.href.split('?')[1]); 114 | } 115 | 116 | if (!this.config.credentials.client) { 117 | return this.getConsumerToken().then(this.authorize.bind(this)); 118 | } 119 | 120 | if (this.config.credentials.token && this.config.credentials.token.public) { 121 | return Promise.resolve("Success"); 122 | } 123 | 124 | if (savedCredentials) { 125 | this.config.credentials = JSON.parse(savedCredentials); 126 | window.localStorage.removeItem('requestTokenCredentials'); 127 | } 128 | 129 | if (!this.config.credentials.token) { 130 | return this.getRequestToken().then(this.authorize.bind(this)); 131 | } else if (!this.config.credentials.token.public && !savedCredentials) { 132 | window.localStorage.setItem('requestTokenCredentials', JSON.stringify(this.config.credentials)); 133 | window.location = next.redirectURL; 134 | throw 'Redirect to authrization page...'; 135 | } else if (!this.config.credentials.token.public && args.oauth_token) { 136 | this.config.credentials.token.public = args.oauth_token; 137 | return this.getAccessToken(args.oauth_verifier); 138 | } 139 | } 140 | }, { 141 | key: 'saveCredentials', 142 | value: function saveCredentials() { 143 | window.localStorage.setItem('tokenCredentials', JSON.stringify(this.config.credentials)); 144 | } 145 | }, { 146 | key: 'removeCredentials', 147 | value: function removeCredentials() { 148 | delete this.config.credentials.token; 149 | window.localStorage.removeItem('tokenCredentials'); 150 | } 151 | }, { 152 | key: 'hasCredentials', 153 | value: function hasCredentials() { 154 | return this.config.credentials && this.config.credentials.client && this.config.credentials.client.public && this.config.credentials.client.secret && this.config.credentials.token && this.config.credentials.token.public && this.config.credentials.token.secret; 155 | } 156 | }, { 157 | key: 'restoreCredentials', 158 | value: function restoreCredentials() { 159 | var savedCredentials = window.localStorage.getItem('tokenCredentials'); 160 | if (savedCredentials) { 161 | this.config.credentials = JSON.parse(savedCredentials); 162 | } 163 | return this; 164 | } 165 | }, { 166 | key: 'get', 167 | value: function get(url, data) { 168 | return this.request('GET', url, data); 169 | } 170 | }, { 171 | key: 'post', 172 | value: function post(url, data) { 173 | return this.request('POST', url, data); 174 | } 175 | }, { 176 | key: 'del', 177 | value: function del(url, data, callback) { 178 | return this.request('DELETE', url, data); 179 | } 180 | }, { 181 | key: 'request', 182 | value: function request(method, url) { 183 | var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; 184 | 185 | if (url.indexOf('http') !== 0) { 186 | url = this.url + url; 187 | } 188 | 189 | if (method === 'GET' && data) { 190 | // must be decoded before being passed to ouath 191 | url += '?' + decodeURIComponent(_qs2.default.stringify(data)); 192 | data = null; 193 | } 194 | 195 | var oauthData = null; 196 | 197 | if (data) { 198 | oauthData = {}; 199 | Object.keys(data).forEach(function (key) { 200 | var value = data[key]; 201 | if (Array.isArray(value)) { 202 | value.forEach(function (val, index) { 203 | return oauthData[key + '[' + index + ']'] = val; 204 | }); 205 | } else if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { 206 | for (var property in value) { 207 | if (value.hasOwnProperty(property)) { 208 | oauthData[key + '[' + property + ']'] = value[property]; 209 | } 210 | } 211 | } else { 212 | oauthData[key] = value; 213 | } 214 | }); 215 | } 216 | 217 | if (this.oauth) { 218 | var oauthData = this.oauth.authorize({ 219 | method: method, 220 | url: url, 221 | data: oauthData 222 | }, this.config.credentials.token ? this.config.credentials.token : null); 223 | } 224 | 225 | var headers = { 226 | Accept: 'application/json', 227 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 228 | }; 229 | 230 | var requestUrls = [this.config.url + 'oauth1/request']; 231 | 232 | /** 233 | * Only attach the oauth headers if we have a request token, or it is a request to the `oauth/request` endpoint. 234 | */ 235 | if (this.oauth && this.config.credentials.token || requestUrls.indexOf(url) > -1) { 236 | headers = _extends({}, headers, this.oauth.toHeader(oauthData)); 237 | } 238 | 239 | return fetch(url, { 240 | method: method, 241 | headers: headers, 242 | mode: 'cors', 243 | body: ['GET', 'HEAD'].indexOf(method) > -1 ? null : _qs2.default.stringify(data) 244 | }).then(function (response) { 245 | if (response.headers.get('Content-Type') && response.headers.get('Content-Type').indexOf('x-www-form-urlencoded') > -1) { 246 | return response.text().then(function (text) { 247 | return _qs2.default.parse(text); 248 | }); 249 | } 250 | return response.text().then(function (text) { 251 | 252 | try { 253 | var json = JSON.parse(text); 254 | } catch (e) { 255 | throw { message: text, code: response.status }; 256 | } 257 | 258 | if (response.status >= 300) { 259 | throw json; 260 | } else { 261 | return json; 262 | } 263 | }); 264 | }); 265 | } 266 | }]); 267 | 268 | return _class; 269 | }(); 270 | 271 | exports.default = _class; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-rest-api-oauth-1", 3 | "version": "1.1.6", 4 | "description": "WordPrest REST API OAuth 1 Client", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "babel src -d ./", 8 | "start": "babel example -d ./" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/WP-API/wordpress-rest-api-oauth-1.git" 13 | }, 14 | "author": "joehoyle", 15 | "license": "MIT", 16 | "dependencies": { 17 | "oauth-1.0a": "^1.0.1", 18 | "qs": "^6.2.0" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.10.1", 22 | "babel-preset-es2015": "^6.9.0", 23 | "babel-preset-stage-2": "^6.11.0" 24 | }, 25 | "babel": { 26 | "presets": [ 27 | "es2015", 28 | "stage-2" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | import oauth from 'oauth-1.0a' 3 | 4 | export default class { 5 | constructor( config ) { 6 | this.url = config.rest_url ? config.rest_url : ( config.url + 'wp-json' ) 7 | this.url = this.url.replace( /\/$/, '' ) 8 | this.credentials = config.credentials 9 | 10 | if ( this.credentials ) { 11 | this.oauth = new oauth({ 12 | consumer: config.credentials.client, 13 | signature_method: 'HMAC-SHA1' 14 | }) 15 | } 16 | this.config = config 17 | } 18 | 19 | getConsumerToken() { 20 | if ( ! this.config.brokerCredentials ) { 21 | throw new Error( 'Config does not include a brokerCredentials value.' ) 22 | } 23 | 24 | this.config.credentials.client = this.config.brokerCredentials.client 25 | return this.post( `${this.config.brokerURL}broker/connect`, { 26 | server_url: this.config.url, 27 | } ).then( data => { 28 | 29 | if ( data.status && data.status === 'error' ) { 30 | throw { message: `Broker error: ${data.message}`, code: data.type } 31 | } 32 | this.config.credentials.client = { 33 | public: data.client_token, 34 | secret: data.client_secret, 35 | } 36 | 37 | return data 38 | } ) 39 | } 40 | 41 | getRequestToken() { 42 | 43 | if ( ! this.config.callbackURL ) { 44 | throw new Error( 'Config does not include a callbackURL value.' ) 45 | } 46 | return this.post( `${this.config.url}oauth1/request`, { 47 | callback_url: this.config.callbackURL, 48 | } ).then( data => { 49 | var redirectURL = `${this.config.url}oauth1/authorize?${qs.stringify({ 50 | oauth_token: data.oauth_token, 51 | oauth_callback: this.config.callbackURL, 52 | })}` 53 | 54 | this.config.credentials.token = { 55 | secret: data.oauth_token_secret, 56 | } 57 | 58 | return { redirectURL: redirectURL, token: data } 59 | } ) 60 | } 61 | 62 | getAccessToken( oauthVerifier ) { 63 | return this.post( `${this.config.url}oauth1/access`, { 64 | oauth_verifier: oauthVerifier, 65 | } ).then( data => { 66 | this.config.credentials.token = { 67 | public: data.oauth_token, 68 | secret: data.oauth_token_secret, 69 | } 70 | 71 | return this.config.credentials.token 72 | } ) 73 | } 74 | 75 | authorize( next ) { 76 | 77 | var args = {} 78 | var savedCredentials = window.localStorage.getItem( 'requestTokenCredentials' ) 79 | if ( window.location.href.indexOf( '?' ) ) { 80 | args = qs.parse( window.location.href.split('?')[1] ) 81 | } 82 | 83 | if ( ! this.config.credentials.client ) { 84 | return this.getConsumerToken().then( this.authorize.bind( this ) ) 85 | } 86 | 87 | if ( this.config.credentials.token && this.config.credentials.token.public ) { 88 | return Promise.resolve("Success") 89 | } 90 | 91 | if ( savedCredentials ) { 92 | this.config.credentials = JSON.parse( savedCredentials ) 93 | window.localStorage.removeItem( 'requestTokenCredentials' ) 94 | } 95 | 96 | if ( ! this.config.credentials.token ) { 97 | return this.getRequestToken().then( this.authorize.bind( this ) ) 98 | } else if ( ! this.config.credentials.token.public && ! savedCredentials ) { 99 | window.localStorage.setItem( 'requestTokenCredentials', JSON.stringify( this.config.credentials ) ) 100 | window.location = next.redirectURL 101 | throw 'Redirect to authrization page...' 102 | } else if ( ! this.config.credentials.token.public && args.oauth_token ) { 103 | this.config.credentials.token.public = args.oauth_token 104 | return this.getAccessToken( args.oauth_verifier ) 105 | } 106 | } 107 | 108 | saveCredentials() { 109 | window.localStorage.setItem( 'tokenCredentials', JSON.stringify( this.config.credentials ) ) 110 | } 111 | 112 | removeCredentials() { 113 | delete this.config.credentials.token 114 | window.localStorage.removeItem( 'tokenCredentials' ) 115 | } 116 | 117 | hasCredentials() { 118 | return this.config.credentials 119 | && this.config.credentials.client 120 | && this.config.credentials.client.public 121 | && this.config.credentials.client.secret 122 | && this.config.credentials.token 123 | && this.config.credentials.token.public 124 | && this.config.credentials.token.secret 125 | } 126 | 127 | restoreCredentials() { 128 | var savedCredentials = window.localStorage.getItem( 'tokenCredentials' ) 129 | if ( savedCredentials ) { 130 | this.config.credentials = JSON.parse( savedCredentials ) 131 | } 132 | return this 133 | } 134 | 135 | get( url, data ) { 136 | return this.request( 'GET', url, data ) 137 | } 138 | 139 | post( url, data ) { 140 | return this.request( 'POST', url, data ) 141 | } 142 | 143 | del( url, data, callback ) { 144 | return this.request( 'DELETE', url, data ) 145 | } 146 | 147 | request( method, url, data = null ) { 148 | if ( url.indexOf( 'http' ) !== 0 ) { 149 | url = this.url + url 150 | } 151 | 152 | if ( method === 'GET' && data ) { 153 | // must be decoded before being passed to ouath 154 | url += `?${decodeURIComponent( qs.stringify(data) )}` 155 | data = null 156 | } 157 | 158 | var oauthData = null 159 | 160 | if ( data ) { 161 | oauthData = {} 162 | Object.keys( data ).forEach( key => { 163 | var value = data[ key ] 164 | if ( Array.isArray( value ) ) { 165 | value.forEach( ( val, index ) => oauthData[ `${key}[${index}]` ] = val ) 166 | } else if( typeof value === 'object' ) { 167 | for (var property in value) { 168 | if (value.hasOwnProperty(property)) { 169 | oauthData[ `${key}[${property}]`] = value[property] 170 | } 171 | } 172 | } else { 173 | oauthData[ key ] = value 174 | } 175 | }) 176 | } 177 | 178 | if ( this.oauth ) { 179 | var oauthData = this.oauth.authorize( { 180 | method: method, 181 | url: url, 182 | data: oauthData 183 | }, this.config.credentials.token ? this.config.credentials.token : null ) 184 | } 185 | 186 | var headers = { 187 | Accept: 'application/json', 188 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 189 | } 190 | 191 | const requestUrls = [ 192 | `${this.config.url}oauth1/request` 193 | ] 194 | 195 | /** 196 | * Only attach the oauth headers if we have a request token, or it is a request to the `oauth/request` endpoint. 197 | */ 198 | if ( this.oauth && this.config.credentials.token || requestUrls.indexOf( url ) > -1 ) { 199 | headers = {...headers, ...this.oauth.toHeader( oauthData )} 200 | } 201 | 202 | return fetch( url, { 203 | method: method, 204 | headers: headers, 205 | mode: 'cors', 206 | body: ['GET','HEAD'].indexOf( method ) > -1 ? null : qs.stringify( data ) 207 | } ) 208 | .then( response => { 209 | if ( response.headers.get( 'Content-Type' ) && response.headers.get( 'Content-Type' ).indexOf( 'x-www-form-urlencoded' ) > -1 ) { 210 | return response.text().then( text => { 211 | return qs.parse( text ) 212 | }) 213 | } 214 | return response.text().then( text => { 215 | 216 | try { 217 | var json = JSON.parse( text ) 218 | } catch( e ) { 219 | throw { message: text, code: response.status } 220 | } 221 | 222 | if ( response.status >= 300) { 223 | throw json 224 | } else { 225 | return json 226 | } 227 | }) 228 | } ) 229 | } 230 | } 231 | --------------------------------------------------------------------------------