source: node_modules/nodemailer/lib/xoauth2/index.js@ 62b2964

finki-main main
Last change on this file since 62b2964 was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

  • Property mode set to 100644
File size: 13.3 KB
RevLine 
[81bc7da]1'use strict';
2
3const Stream = require('stream').Stream;
4const nmfetch = require('../fetch');
5const crypto = require('crypto');
6const shared = require('../shared');
7
8/**
9 * XOAUTH2 access_token generator for Gmail.
10 * Create client ID for web applications in Google API console to use it.
11 * See Offline Access for receiving the needed refreshToken for an user
12 * https://developers.google.com/accounts/docs/OAuth2WebServer#offline
13 *
14 * Usage for generating access tokens with a custom method using provisionCallback:
15 * provisionCallback(user, renew, callback)
16 * * user is the username to get the token for
17 * * renew is a boolean that if true indicates that existing token failed and needs to be renewed
18 * * callback is the callback to run with (error, accessToken [, expires])
19 * * accessToken is a string
20 * * expires is an optional expire time in milliseconds
21 * If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself
22 *
23 * @constructor
24 * @param {Object} options Client information for token generation
25 * @param {String} options.user User e-mail address
26 * @param {String} options.clientId Client ID value
27 * @param {String} options.clientSecret Client secret value
28 * @param {String} options.refreshToken Refresh token for an user
29 * @param {String} options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
30 * @param {String} options.accessToken An existing valid accessToken
31 * @param {String} options.privateKey Private key for JSW
32 * @param {Number} options.expires Optional Access Token expire time in ms
33 * @param {Number} options.timeout Optional TTL for Access Token in seconds
34 * @param {Function} options.provisionCallback Function to run when a new access token is required
35 */
36class XOAuth2 extends Stream {
37 constructor(options, logger) {
38 super();
39
40 this.options = options || {};
41
42 if (options && options.serviceClient) {
43 if (!options.privateKey || !options.user) {
44 setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
45 return;
46 }
47
48 let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
49 this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
50 }
51
52 this.logger = shared.getLogger(
53 {
54 logger
55 },
56 {
57 component: this.options.component || 'OAuth2'
58 }
59 );
60
61 this.provisionCallback = typeof this.options.provisionCallback === 'function' ? this.options.provisionCallback : false;
62
63 this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token';
64 this.options.customHeaders = this.options.customHeaders || {};
65 this.options.customParams = this.options.customParams || {};
66
67 this.accessToken = this.options.accessToken || false;
68
69 if (this.options.expires && Number(this.options.expires)) {
70 this.expires = this.options.expires;
71 } else {
72 let timeout = Math.max(Number(this.options.timeout) || 0, 0);
73 this.expires = (timeout && Date.now() + timeout * 1000) || 0;
74 }
75 }
76
77 /**
78 * Returns or generates (if previous has expired) a XOAuth2 token
79 *
80 * @param {Boolean} renew If false then use cached access token (if available)
81 * @param {Function} callback Callback function with error object and token string
82 */
83 getToken(renew, callback) {
84 if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
85 return callback(null, this.accessToken);
86 }
87
88 let generateCallback = (...args) => {
89 if (args[0]) {
90 this.logger.error(
91 {
92 err: args[0],
93 tnx: 'OAUTH2',
94 user: this.options.user,
95 action: 'renew'
96 },
97 'Failed generating new Access Token for %s',
98 this.options.user
99 );
100 } else {
101 this.logger.info(
102 {
103 tnx: 'OAUTH2',
104 user: this.options.user,
105 action: 'renew'
106 },
107 'Generated new Access Token for %s',
108 this.options.user
109 );
110 }
111 callback(...args);
112 };
113
114 if (this.provisionCallback) {
115 this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => {
116 if (!err && accessToken) {
117 this.accessToken = accessToken;
118 this.expires = expires || 0;
119 }
120 generateCallback(err, accessToken);
121 });
122 } else {
123 this.generateToken(generateCallback);
124 }
125 }
126
127 /**
128 * Updates token values
129 *
130 * @param {String} accessToken New access token
131 * @param {Number} timeout Access token lifetime in seconds
132 *
133 * Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
134 */
135 updateToken(accessToken, timeout) {
136 this.accessToken = accessToken;
137 timeout = Math.max(Number(timeout) || 0, 0);
138 this.expires = (timeout && Date.now() + timeout * 1000) || 0;
139
140 this.emit('token', {
141 user: this.options.user,
142 accessToken: accessToken || '',
143 expires: this.expires
144 });
145 }
146
147 /**
148 * Generates a new XOAuth2 token with the credentials provided at initialization
149 *
150 * @param {Function} callback Callback function with error object and token string
151 */
152 generateToken(callback) {
153 let urlOptions;
154 let loggedUrlOptions;
155 if (this.options.serviceClient) {
156 // service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
157 let iat = Math.floor(Date.now() / 1000); // unix time
158 let tokenData = {
159 iss: this.options.serviceClient,
160 scope: this.options.scope || 'https://mail.google.com/',
161 sub: this.options.user,
162 aud: this.options.accessUrl,
163 iat,
164 exp: iat + this.options.serviceRequestTimeout
165 };
166 let token;
167 try {
168 token = this.jwtSignRS256(tokenData);
169 } catch (err) {
170 return callback(new Error('Can\x27t generate token. Check your auth options'));
171 }
172
173 urlOptions = {
174 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
175 assertion: token
176 };
177
178 loggedUrlOptions = {
179 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
180 assertion: tokenData
181 };
182 } else {
183 if (!this.options.refreshToken) {
184 return callback(new Error('Can\x27t create new access token for user'));
185 }
186
187 // web app - https://developers.google.com/identity/protocols/OAuth2WebServer
188 urlOptions = {
189 client_id: this.options.clientId || '',
190 client_secret: this.options.clientSecret || '',
191 refresh_token: this.options.refreshToken,
192 grant_type: 'refresh_token'
193 };
194
195 loggedUrlOptions = {
196 client_id: this.options.clientId || '',
197 client_secret: (this.options.clientSecret || '').substr(0, 6) + '...',
198 refresh_token: (this.options.refreshToken || '').substr(0, 6) + '...',
199 grant_type: 'refresh_token'
200 };
201 }
202
203 Object.keys(this.options.customParams).forEach(key => {
204 urlOptions[key] = this.options.customParams[key];
205 loggedUrlOptions[key] = this.options.customParams[key];
206 });
207
208 this.logger.debug(
209 {
210 tnx: 'OAUTH2',
211 user: this.options.user,
212 action: 'generate'
213 },
214 'Requesting token using: %s',
215 JSON.stringify(loggedUrlOptions)
216 );
217
218 this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => {
219 let data;
220
221 if (error) {
222 return callback(error);
223 }
224
225 try {
226 data = JSON.parse(body.toString());
227 } catch (E) {
228 return callback(E);
229 }
230
231 if (!data || typeof data !== 'object') {
232 this.logger.debug(
233 {
234 tnx: 'OAUTH2',
235 user: this.options.user,
236 action: 'post'
237 },
238 'Response: %s',
239 (body || '').toString()
240 );
241 return callback(new Error('Invalid authentication response'));
242 }
243
244 let logData = {};
245 Object.keys(data).forEach(key => {
246 if (key !== 'access_token') {
247 logData[key] = data[key];
248 } else {
249 logData[key] = (data[key] || '').toString().substr(0, 6) + '...';
250 }
251 });
252
253 this.logger.debug(
254 {
255 tnx: 'OAUTH2',
256 user: this.options.user,
257 action: 'post'
258 },
259 'Response: %s',
260 JSON.stringify(logData)
261 );
262
263 if (data.error) {
264 // Error Response : https://tools.ietf.org/html/rfc6749#section-5.2
265 let errorMessage = data.error;
266 if (data.error_description) {
267 errorMessage += ': ' + data.error_description;
268 }
269 if (data.error_uri) {
270 errorMessage += ' (' + data.error_uri + ')';
271 }
272 return callback(new Error(errorMessage));
273 }
274
275 if (data.access_token) {
276 this.updateToken(data.access_token, data.expires_in);
277 return callback(null, this.accessToken);
278 }
279
280 return callback(new Error('No access token'));
281 });
282 }
283
284 /**
285 * Converts an access_token and user id into a base64 encoded XOAuth2 token
286 *
287 * @param {String} [accessToken] Access token string
288 * @return {String} Base64 encoded token for IMAP or SMTP login
289 */
290 buildXOAuth2Token(accessToken) {
291 let authData = ['user=' + (this.options.user || ''), 'auth=Bearer ' + (accessToken || this.accessToken), '', ''];
292 return Buffer.from(authData.join('\x01'), 'utf-8').toString('base64');
293 }
294
295 /**
296 * Custom POST request handler.
297 * This is only needed to keep paths short in Windows – usually this module
298 * is a dependency of a dependency and if it tries to require something
299 * like the request module the paths get way too long to handle for Windows.
300 * As we do only a simple POST request we do not actually require complicated
301 * logic support (no redirects, no nothing) anyway.
302 *
303 * @param {String} url Url to POST to
304 * @param {String|Buffer} payload Payload to POST
305 * @param {Function} callback Callback function with (err, buff)
306 */
307 postRequest(url, payload, params, callback) {
308 let returned = false;
309
310 let chunks = [];
311 let chunklen = 0;
312
313 let req = nmfetch(url, {
314 method: 'post',
315 headers: params.customHeaders,
316 body: payload,
317 allowErrorResponse: true
318 });
319
320 req.on('readable', () => {
321 let chunk;
322 while ((chunk = req.read()) !== null) {
323 chunks.push(chunk);
324 chunklen += chunk.length;
325 }
326 });
327
328 req.once('error', err => {
329 if (returned) {
330 return;
331 }
332 returned = true;
333 return callback(err);
334 });
335
336 req.once('end', () => {
337 if (returned) {
338 return;
339 }
340 returned = true;
341 return callback(null, Buffer.concat(chunks, chunklen));
342 });
343 }
344
345 /**
346 * Encodes a buffer or a string into Base64url format
347 *
348 * @param {Buffer|String} data The data to convert
349 * @return {String} The encoded string
350 */
351 toBase64URL(data) {
352 if (typeof data === 'string') {
353 data = Buffer.from(data);
354 }
355
356 return data
357 .toString('base64')
358 .replace(/[=]+/g, '') // remove '='s
359 .replace(/\+/g, '-') // '+' → '-'
360 .replace(/\//g, '_'); // '/' → '_'
361 }
362
363 /**
364 * Creates a JSON Web Token signed with RS256 (SHA256 + RSA)
365 *
366 * @param {Object} payload The payload to include in the generated token
367 * @return {String} The generated and signed token
368 */
369 jwtSignRS256(payload) {
370 payload = ['{"alg":"RS256","typ":"JWT"}', JSON.stringify(payload)].map(val => this.toBase64URL(val)).join('.');
371 let signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
372 return payload + '.' + this.toBase64URL(signature);
373 }
374}
375
376module.exports = XOAuth2;
Note: See TracBrowser for help on using the repository browser.