Skip to content

Commit d389d49

Browse files
Deploy full oauth functionality (#99)
* Fix encodeURI params and some clean up (#98) (#4) * v2.7.7 * fix encodeURI issue * version bump Co-authored-by: Ajaykumar <pajay2507@gmail.com> * Add OAUTH endpoint to constants * Add full oauth support * Add oauth endpoint to expected properties * Rename access_token -> appAccessToken * Remove oauth functions from utils * Add user token demo * Fix missing context var * Add missing function description * Add urldecode declaration to docstring * Remove trailling spaces * Migrate credential functions to credentials.js Co-authored-by: Ajaykumar <pajay2507@gmail.com>
1 parent ffa4bf3 commit d389d49

File tree

8 files changed

+232
-43
lines changed

8 files changed

+232
-43
lines changed

demo/getUserToken.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const readline = require('readline');
2+
const Ebay = require('../src/index');
3+
const { clientId, clientSecret, redirectUri } = require('./credentials/index');
4+
5+
let ebay = new Ebay({
6+
clientID: clientId,
7+
clientSecret: clientSecret,
8+
redirectUri: redirectUri,
9+
body: {
10+
grant_type: 'authorization_code',
11+
scope: 'https://api.ebay.com/oauth/api_scope'
12+
}
13+
});
14+
15+
const rl = readline.createInterface({
16+
input: process.stdin,
17+
output: process.stdout,
18+
});
19+
20+
const authURL = ebay.getUserAuthorizationUrl();
21+
console.log(`Please go here for auth code: ${authURL}`);
22+
rl.question("Enter the auth code recieved from the redirect url (should urldecode it first): ", code => {
23+
rl.close();
24+
ebay.getUserTokenByCode(code).then(data => {
25+
console.log('User token by code response:-');
26+
console.log(data);
27+
ebay.getUserTokenByRefresh().then(data => {
28+
console.log('User token by refresh token response:-');
29+
console.log(data);
30+
});
31+
})
32+
});

src/buy-api.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ const { encodeURLQuery, base64Encode } = require('./common-utils');
66

77
const getItem = function (itemId) {
88
if (!itemId) throw new Error('Item Id is required');
9-
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
10-
const auth = 'Bearer ' + this.options.access_token;
9+
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
10+
const auth = 'Bearer ' + this.options.appAccessToken;
1111
const id = encodeURIComponent(itemId);
1212
this.options.contentType = 'application/json';
1313
return makeRequest(this.options, `/buy/browse/v1/item/${id}`, 'GET', auth).then((result) => {
@@ -17,9 +17,9 @@ const getItem = function (itemId) {
1717

1818
const getItemByLegacyId = function (legacyOptions) {
1919
if (!legacyOptions) throw new Error('Error Required input to get Items By LegacyID');
20-
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
20+
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
2121
if (!legacyOptions.legacyItemId) throw new Error('Error Legacy Item Id is required');
22-
const auth = 'Bearer ' + this.options.access_token;
22+
const auth = 'Bearer ' + this.options.appAccessToken;
2323
let param = 'legacy_item_id=' + legacyOptions.legacyItemId;
2424
param += legacyOptions.legacyVariationSku ? '&legacy_variation_sku=' + legacyOptions.legacyVariationSku : '';
2525
this.options.contentType = 'application/json';
@@ -35,8 +35,8 @@ const getItemByLegacyId = function (legacyOptions) {
3535
const getItemByItemGroup = function (itemGroupId) {
3636
if (typeof itemGroupId === 'object') throw new Error('Expecting String or number (Item group id)');
3737
if (!itemGroupId) throw new Error('Error Item Group ID is required');
38-
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
39-
const auth = 'Bearer ' + this.options.access_token;
38+
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
39+
const auth = 'Bearer ' + this.options.appAccessToken;
4040
this.options.contentType = 'application/json';
4141
return new Promise((resolve, reject) => {
4242
makeRequest(this.options, `/buy/browse/v1/item/get_items_by_item_group?item_group_id=${itemGroupId}`, 'GET', auth).then((result) => {
@@ -50,8 +50,8 @@ const getItemByItemGroup = function (itemGroupId) {
5050
const searchItems = function (searchConfig) {
5151
if (!searchConfig) throw new Error('Error --> Missing or invalid input parameter to search');
5252
if (!searchConfig.keyword && !searchConfig.categoryId && !searchConfig.gtin) throw new Error('Error --> Keyword or category id is required in query param');
53-
if (!this.options.access_token) throw new Error('Error -->Missing Access token, Generate access token');
54-
const auth = 'Bearer ' + this.options.access_token;
53+
if (!this.options.appAccessToken) throw new Error('Error -->Missing Access token, Generate access token');
54+
const auth = 'Bearer ' + this.options.appAccessToken;
5555
let queryParam = searchConfig.keyword ? 'q=' + encodeURIComponent(searchConfig.keyword) : '';
5656
queryParam = queryParam + (searchConfig.gtin ? '&gtin=' + searchConfig.gtin : '');
5757
queryParam = queryParam + (searchConfig.categoryId ? '&category_ids=' + searchConfig.categoryId : '');
@@ -73,9 +73,9 @@ const searchItems = function (searchConfig) {
7373

7474
const searchByImage = function (searchConfig) {
7575
if (!searchConfig) throw new Error('INVALID_REQUEST_PARMS --> Missing or invalid input parameter to search by image');
76-
if (!this.options.access_token) throw new Error('INVALID_AUTH_TOKEN --> Missing Access token, Generate access token');
76+
if (!this.options.appAccessToken) throw new Error('INVALID_AUTH_TOKEN --> Missing Access token, Generate access token');
7777
if (!searchConfig.imgPath && !searchConfig.base64Image) throw new Error('REQUIRED_PARAMS --> imgPath or base64Image is required');
78-
const auth = 'Bearer ' + this.options.access_token;
78+
const auth = 'Bearer ' + this.options.appAccessToken;
7979
const encodeImage = searchConfig.imgPath ? base64Encode(fs.readFileSync(searchConfig.imgPath)) : searchConfig.base64Image;
8080
this.options.data = JSON.stringify({ image: encodeImage });
8181
this.options.contentType = 'application/json';

src/common-utils/index.js

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22
const { makeRequest } = require('../request');
33

4-
const base64Encode = (encodeData) => {
4+
const base64Encode = encodeData => {
55
const buff = Buffer.from(encodeData);;
66
return buff.toString('base64');
77
};
@@ -46,23 +46,6 @@ const constructAdditionalParams = (options) => {
4646
};
4747

4848
module.exports = {
49-
setAccessToken: function (token) {
50-
this.options.access_token = token;
51-
},
52-
getAccessToken: function () {
53-
if (!this.options.clientID) throw new Error('Missing Client ID');
54-
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
55-
if (!this.options.body) throw new Error('Missing Body, required Grant type');
56-
const encodedStr = base64Encode(this.options.clientID + ':' + this.options.clientSecret);
57-
const self = this;
58-
const auth = 'Basic ' + encodedStr;
59-
this.options.contentType = 'application/x-www-form-urlencoded';
60-
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then((result) => {
61-
const resultJSON = JSON.parse(result);
62-
self.setAccessToken(resultJSON.access_token);
63-
return resultJSON;
64-
});
65-
},
6649
setSiteId: function (siteId) {
6750
this.options.siteId = siteId;
6851
},
@@ -73,10 +56,7 @@ module.exports = {
7356
if (!isString(data)) data = data.toString();
7457
return data.toUpperCase();
7558
},
76-
77-
// Returns if a value is a string
7859
isString,
79-
8060
// Returns if object is empty or not
8161
isEmptyObj(obj) {
8262
for (let key in obj) {

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22

33
module.exports = {
4+
PROD_OAUTHENVIRONMENT_WEBENDPOINT: 'https://auth.ebay.com/oauth2/authorize',
5+
SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT: 'https://auth.sandbox.ebay.com/oauth2/authorize',
46
PROD_BASE_URL: 'api.ebay.com',
57
SANDBOX_BASE_URL: 'api.sandbox.ebay.com',
68
BASE_SVC_URL: 'svcs.ebay.com',

src/credentials.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use strict';
2+
const qs = require('querystring');
3+
const { base64Encode } = require('./common-utils');
4+
const { makeRequest } = require('./request');
5+
const DEFAULT_API_SCOPE = 'https://api.ebay.com/oauth/api_scope';
6+
7+
/**
8+
* Generates an application access token for client credentials grant flow
9+
*
10+
* @return appAccessToken object
11+
*/
12+
const getAccessToken = function () {
13+
if (!this.options.clientID) throw new Error('Missing Client ID');
14+
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
15+
if (!this.options.body) throw new Error('Missing Body, required Grant type');
16+
let scopesParam = this.options.body.scopes
17+
? Array.isArray(this.options.body.scopes)
18+
? this.options.body.scopes.join('%20')
19+
: this.options.body.scopes
20+
: DEFAULT_API_SCOPE;
21+
this.options.data = qs.stringify({
22+
grant_type: 'client_credentials',
23+
scope: scopesParam
24+
});
25+
this.options.contentType = 'application/x-www-form-urlencoded';
26+
const self = this;
27+
const encodedStr = base64Encode(this.options.clientID + ':' + this.options.clientSecret);
28+
const auth = 'Basic ' + encodedStr;
29+
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then((result) => {
30+
const resultJSON = JSON.parse(result);
31+
if (!resultJSON.error) self.setAppAccessToken(resultJSON);
32+
return resultJSON;
33+
});
34+
};
35+
36+
/**
37+
* Generates user consent authorization url
38+
*
39+
* @param state custom state value
40+
* @return userConsentUrl
41+
*/
42+
const getUserAuthorizationUrl = function (state = null) {
43+
if (!this.options.clientID) throw new Error('Missing Client ID');
44+
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
45+
if (!this.options.body) throw new Error('Missing Body, required Grant type');
46+
if (!this.options.redirectUri) throw new Error('redirect_uri is required for redirection after sign in\nkindly check here https://developer.ebay.com/api-docs/static/oauth-redirect-uri.html');
47+
let scopesParam = this.options.body.scopes
48+
? Array.isArray(this.options.body.scopes)
49+
? this.options.body.scopes.join('%20')
50+
: this.options.body.scopes
51+
: DEFAULT_API_SCOPE;
52+
let queryParam = `client_id=${this.options.clientID}`;
53+
queryParam += `&redirect_uri=${this.options.redirectUri}`;
54+
queryParam += `&response_type=code`;
55+
queryParam += `&scope=${scopesParam}`;
56+
queryParam += state ? `&state=${state}` : '';
57+
return `${this.options.oauthEndpoint}?${queryParam}`;
58+
};
59+
60+
/**
61+
* Generates a User access token given auth code
62+
*
63+
* @param code code generated from browser using the method getUserAuthorizationUrl (should be urldecoded)
64+
* @return userAccessToken object (with refresh_token)
65+
*/
66+
const getUserTokenByCode = function (code) {
67+
if (!code) throw new Error('Authorization code is required, to generate authorization code use getUserAuthorizationUrl method');
68+
if (!this.options.clientID) throw new Error('Missing Client ID');
69+
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
70+
if (!this.options.redirectUri) throw new Error('redirect_uri is required for redirection after sign in\nkindly check here https://developer.ebay.com/api-docs/static/oauth-redirect-uri.html');
71+
this.options.data = qs.stringify({
72+
code: code,
73+
grant_type: 'authorization_code',
74+
redirect_uri: this.options.redirectUri
75+
});
76+
this.options.contentType = 'application/x-www-form-urlencoded';
77+
const self = this;
78+
const encodedStr = base64Encode(`${this.options.clientID}:${this.options.clientSecret}`);
79+
const auth = `Basic ${encodedStr}`;
80+
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then(result => {
81+
const resultJSON = JSON.parse(result);
82+
if (!resultJSON.error) self.setUserAccessToken(resultJSON);
83+
return resultJSON;
84+
});
85+
};
86+
87+
/**
88+
* Use a refresh token to update a User access token (Updating the expired access token)
89+
*
90+
* @param refreshToken refresh token, defaults to pre-assigned refresh token
91+
* @param scopes array of scopes for the access token
92+
* @return userAccessToken object (without refresh_token)
93+
*/
94+
const getUserTokenByRefresh = function (refreshToken = null) {
95+
if (!this.options.clientID) throw new Error('Missing Client ID');
96+
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
97+
if (!this.options.body) throw new Error('Missing Body, required Grant type');
98+
if (!refreshToken && !this.options.refreshToken) {
99+
throw new Error('Refresh token is required, to generate refresh token use getUserTokenByCode method'); // eslint-disable-line max-len
100+
}
101+
refreshToken = refreshToken ? refreshToken : this.options.refreshToken;
102+
let scopesParam = this.options.body.scopes
103+
? Array.isArray(this.options.body.scopes)
104+
? this.options.body.scopes.join('%20')
105+
: this.options.body.scopes
106+
: DEFAULT_API_SCOPE;
107+
this.options.data = qs.stringify({
108+
refresh_token: refreshToken,
109+
grant_type: 'refresh_token',
110+
scope: scopesParam
111+
});
112+
this.options.contentType = 'application/x-www-form-urlencoded';
113+
const self = this;
114+
const encodedStr = base64Encode(`${this.options.clientID}:${this.options.clientSecret}`);
115+
const auth = `Basic ${encodedStr}`;
116+
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then(result => {
117+
const resultJSON = JSON.parse(result);
118+
if (!resultJSON.error) self.setUserAccessToken(resultJSON);
119+
return resultJSON;
120+
});
121+
};
122+
123+
/**
124+
* Assign user access token and refresh token returned from authorization grant workflow (i.e getUserTokenByRefresh)
125+
*
126+
* @param userAccessToken userAccessToken obj returned from getUserTokenByCode or getAccessTokenByRefresh
127+
*/
128+
const setUserAccessToken = function (userAccessToken) {
129+
if (!userAccessToken.token_type === 'User Access Token') throw new Error('userAccessToken is either missing or invalid');
130+
if (userAccessToken.refresh_token) this.options.refreshToken = userAccessToken.refresh_token;
131+
this.options.userAccessToken = userAccessToken.access_token;
132+
};
133+
134+
/**
135+
* Assign application access token returned from client credentials workflow (i.e getAccessToken)
136+
*
137+
* @param appAccessToken appAccessToken obj returned from getApplicationToken
138+
*/
139+
const setAppAccessToken = function (appAccessToken) {
140+
if (!appAccessToken.token_type === 'Application Access Token') throw new Error('appAccessToken is either missing or invalid');
141+
this.options.appAccessToken = appAccessToken.access_token;
142+
};
143+
144+
module.exports = {
145+
getAccessToken,
146+
getUserAuthorizationUrl,
147+
getUserTokenByCode,
148+
getUserTokenByRefresh,
149+
setUserAccessToken,
150+
setAppAccessToken
151+
};

src/index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ const taxonomyApi = require('./taxonomy-api');
55
const ebayFindingApi = require('./finding');
66
const commonUtils = require('./common-utils');
77
const { getSimilarItems, getMostWatchedItems } = require('./merchandising');
8-
const { PROD_BASE_URL, SANDBOX_BASE_URL, BASE_SANDBX_SVC_URL, BASE_SVC_URL } = require('./constants');
8+
const {
9+
getAccessToken,
10+
getUserAuthorizationUrl,
11+
getUserTokenByCode,
12+
getUserTokenByRefresh,
13+
setAppAccessToken,
14+
setUserAccessToken
15+
} = require('./credentials');
16+
const {
17+
PROD_OAUTHENVIRONMENT_WEBENDPOINT,
18+
SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT,
19+
PROD_BASE_URL,
20+
SANDBOX_BASE_URL,
21+
BASE_SANDBX_SVC_URL,
22+
BASE_SVC_URL
23+
} = require('./constants');
924
const PROD_ENV = 'PROD';
1025
const SANDBOX_ENV = 'SANDBOX';
1126

@@ -15,23 +30,25 @@ const SANDBOX_ENV = 'SANDBOX';
1530
*
1631
* @param {Object} options configuration options
1732
* @param {String} options.clientID Client Id/App id
33+
* @param {String} options.clientSecret eBay Secret/Cert ID - required for user access tokens
1834
* @param {String} options.env Environment, defaults to PROD
1935
* @param {String} options.headers HTTP request headers
2036
* @constructor
2137
* @public
2238
*/
23-
2439
function Ebay(options) {
2540
if (!options) throw new Error('Options is missing, please provide the input');
2641
if (!options.clientID) throw Error('Client ID is Missing\ncheck documentation to get Client ID http://developer.ebay.com/DevZone/account/');
2742
if (!(this instanceof Ebay)) return new Ebay(options);
2843
if (!options.env) options.env = PROD_ENV;
2944
options.baseUrl = PROD_BASE_URL;
3045
options.baseSvcUrl = BASE_SVC_URL;
46+
options.oauthEndpoint = PROD_OAUTHENVIRONMENT_WEBENDPOINT;
3147
// handle sandbox env.
3248
if (options.env === SANDBOX_ENV) {
3349
options.baseUrl = SANDBOX_BASE_URL;
3450
options.baseSvcUrl = BASE_SANDBX_SVC_URL;
51+
options.oauthEndpoint = SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT;
3552
}
3653
this.options = options;
3754
commonUtils.setHeaders(this, options.headers);
@@ -40,6 +57,12 @@ function Ebay(options) {
4057
}
4158

4259
Ebay.prototype = {
60+
getAccessToken,
61+
getUserAuthorizationUrl,
62+
getUserTokenByCode,
63+
getUserTokenByRefresh,
64+
setUserAccessToken,
65+
setAppAccessToken,
4366
getMostWatchedItems,
4467
getSimilarItems,
4568
...commonUtils,

0 commit comments

Comments
 (0)