-
Notifications
You must be signed in to change notification settings - Fork 46
/
access_token.js
244 lines (223 loc) · 8.23 KB
/
access_token.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import crypto from 'crypto';
import fs from 'fs';
import got from 'got';
import path from 'path';
import { ApiCommon } from './api_common.js';
import { Scope } from './oauth_scope.js';
import get_auth_code from './user_auth.js';
export class AccessToken extends ApiCommon {
constructor(api_config, { name }) {
super(api_config);
const auth = `${api_config.app_id}:${api_config.app_secret}`;
const b64auth = Buffer.from(auth).toString('base64');
this.api_uri = api_config.api_uri;
this.auth_headers = { Authorization: `Basic ${b64auth}` };
this.name = name || 'access_token';
this.path = path.join(api_config.oauth_token_dir, `${this.name}.json`);
}
/**
* This method tries to make it as easy as possible for a developer
* to start using an OAuth access token. It fetches the access token
* by trying all supported methods, in this order:
* 1. Get from the process environment variable that is the UPPER CASE
* version of the this.name attribute. This method is intended as
* a quick hack for developers.
* 2. Read the access_token and (if available) the refresh_token from
* the file at the path specified by joining the configured
* OAuth token directory, the this.name attribute, and the '.json'
* file extension.
* 3. Execute the OAuth 2.0 request flow using the default browser
* and local redirect.
*/
async fetch({ scopes = null, client_credentials = false }) {
try {
this.from_environment();
return;
} catch (err) {
console.log(`reading ${this.name} from environment failed, trying read`);
if (this.api_config.verbosity >= 3) {
console.log(' because...', err);
}
}
try {
this.read();
return;
} catch (err) {
console.log(`reading ${this.name} failed, trying oauth`);
if (this.api_config.verbosity >= 3) {
console.log(' because...', err);
}
}
await this.oauth({ scopes, client_credentials });
}
/**
* Easiest path for using an access token: get it from the
* process environment. Note that the environment variable name
* is the UPPER CASE of the this.name instance attribute.
*/
from_environment() {
const env_access_token = process.env[this.name.toUpperCase()];
if (env_access_token) {
this.access_token = env_access_token;
this.refresh_token = null;
} else {
throw new Error('No access token in the environment');
}
}
/* Get the access token from the file at this.path. */
read() {
const data = JSON.parse(fs.readFileSync(this.path));
this.name = data.name || 'access_token';
const access_token = data.access_token;
if (!access_token) {
throw new Error('Access token not found in JSON file');
}
this.access_token = access_token;
this.refresh_token = data.refresh_token;
this.scopes = data.scopes;
console.log(`read ${this.name} from ${this.path}`);
}
/* Store the access token in the file at this.path. */
write() {
const json = JSON.stringify({
name: this.name,
access_token: this.access_token,
refresh_token: this.refresh_token,
scopes: this.scopes
}, null, 2);
/* Make credentials-bearing file as secure as possible with mode 0o600. */
fs.open(this.path, 'w', 0o600, (err, fd) => {
if (err) {
throw new Error(`Can not open file for write: ${this.path}`);
}
fs.write(fd, json, (err, written, string) => {
if (err) {
throw new Error(`Can not write file: ${this.path}`);
}
});
});
}
header(headers = {}) {
headers.Authorization = `Bearer ${this.access_token}`;
return headers;
}
hashed() {
return crypto.createHash('sha256').update(this.access_token).digest('hex');
}
/**
* Print the refresh token in a human-readable format that does not reveal
* the actual access credential. The purpose of this method is for a developer
* to verify when the refresh token changes.
*/
hashed_refresh_token() {
if (!this.refresh_token) {
throw new Error('AccessToken does not have a refresh token');
}
return crypto.createHash('sha256').update(this.refresh_token).digest('hex');
}
/**
* When requesting an OAuth token for a user, the protocol requires going
* through the process of getting an auth_code. This process allows the
* user to approve the scopes requested by the application.
*/
async get_user_post_data(scopes) {
console.log('getting auth_code...');
const auth_code = await get_auth_code(this.api_config, { scopes: scopes });
console.log('exchanging auth_code for access_token...');
return {
code: auth_code,
redirect_uri: this.api_config.redirect_uri,
grant_type: 'authorization_code'
};
}
/**
* When requesting an OAuth token for the client, no auth_code is required
* because the user is the same as the owner of the client.
*/
get_client_post_data(scopes) {
console.log('getting access token using client credentials...');
return { grant_type: 'client_credentials', scope: scopes.map(s => s.value) };
}
/**
* Execute the OAuth 2.0 process for obtaining an access token.
* For more information, see IETF RFC 6749: https://tools.ietf.org/html/rfc6749
* and https://developers.pinterest.com/docs/getting-started/authentication-and-scopes/
*
* Constructor may not be async, so OAuth must be performed as a separate method.
*/
async oauth({ scopes = null, client_credentials = false }) {
if (!scopes) {
scopes = [Scope.READ_USERS, Scope.READ_PINS, Scope.READ_BOARDS];
console.log('OAuth scopes required. Setting to default: READ_USERS,READ_PINS,READ_BOARDS');
}
// Construct the POST data for the request and output the relevant console message.
// In the case of a user token, getting an auth_code through a manual process
// is required.
const post_data = client_credentials
? this.get_client_post_data(scopes)
: await this.get_user_post_data(scopes);
try {
if (this.api_config.verbosity >= 2) {
console.log('POST', `${this.api_uri}/v5/oauth/token`);
if (this.api_config.verbosity >= 3) {
this.api_config.credentials_warning();
console.log(post_data);
}
}
const response = await got.post(`${this.api_uri}/v5/oauth/token`, {
headers: this.auth_headers, // use the recommended authorization approach
form: post_data, // send body as x-www-form-urlencoded
responseType: 'json'
});
this.print_response(response);
// The scope returned in the response includes all of the scopes that
// have been approved now or in the past by the user.
console.log('scope:', response.body.scope);
this.scopes = response.body.scope;
this.access_token = response.body.access_token;
this.refresh_token = response.body.refresh_token;
if (this.refresh_token) {
console.log('received refresh token');
}
} catch (error) {
this.print_and_throw_error(error);
}
}
async refresh({ continuous = false }) {
// There should be a refresh_token, but it is best to check.
if (!this.refresh_token) {
throw new Error('AccessToken does not have a refresh token');
}
console.log('refreshing access_token...');
let response;
try {
const post_data = {
grant_type: 'refresh_token',
refresh_token: this.refresh_token
};
if (continuous) {
post_data.refresh_on = true;
}
if (this.api_config.verbosity >= 2) {
console.log('POST', `${this.api_uri}/v5/oauth/token`);
if (this.api_config.verbosity >= 3) {
this.api_config.credentials_warning();
console.log(post_data);
}
}
response = await got.post(`${this.api_uri}/v5/oauth/token`, {
headers: this.auth_headers,
form: post_data, // send body as x-www-form-urlencoded
responseType: 'json'
});
this.print_response(response);
this.access_token = response.body.access_token;
if (response.body.refresh_token) {
console.log('received refresh token');
this.refresh_token = response.body.refresh_token;
}
} catch (error) {
this.print_and_throw_error(error);
}
}
}