From 3ab2548b872a489e87c82408d1f64a68443ddbbc Mon Sep 17 00:00:00 2001 From: Steven Ickman Date: Tue, 23 Jan 2018 14:53:54 -0800 Subject: [PATCH] fixed race condition when refreshing access token. (#4084) --- Node/core/lib/bots/ChatConnector.js | 61 +++++++++++++++------------- Node/core/src/bots/ChatConnector.ts | 63 ++++++++++++++++------------- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/Node/core/lib/bots/ChatConnector.js b/Node/core/lib/bots/ChatConnector.js index f13a15cf2b..3bdd5befdc 100644 --- a/Node/core/lib/bots/ChatConnector.js +++ b/Node/core/lib/bots/ChatConnector.js @@ -8,6 +8,7 @@ var request = require("request"); var async = require("async"); var jwt = require("jsonwebtoken"); var zlib = require("zlib"); +var Promise = require("promise"); var urlJoin = require("url-join"); var pjson = require('../../package.json'); var MAX_DATA_LENGTH = 65000; @@ -569,33 +570,39 @@ var ChatConnector = (function () { }; ChatConnector.prototype.refreshAccessToken = function (cb) { var _this = this; - var opt = { - method: 'POST', - url: this.settings.endpoint.refreshEndpoint, - form: { - grant_type: 'client_credentials', - client_id: this.settings.appId, - client_secret: this.settings.appPassword, - scope: this.settings.endpoint.refreshScope - } - }; - this.addUserAgent(opt); - request(opt, function (err, response, body) { - if (!err) { - if (body && response.statusCode < 300) { - var oauthResponse = JSON.parse(body); - _this.accessToken = oauthResponse.access_token; - _this.accessTokenExpires = new Date().getTime() + ((oauthResponse.expires_in - 300) * 1000); - cb(null, _this.accessToken); - } - else { - cb(new Error('Refresh access token failed with status code: ' + response.statusCode), null); - } - } - else { - cb(err, null); - } - }); + if (!this.refreshingToken) { + this.refreshingToken = new Promise(function (resolve, reject) { + var opt = { + method: 'POST', + url: _this.settings.endpoint.refreshEndpoint, + form: { + grant_type: 'client_credentials', + client_id: _this.settings.appId, + client_secret: _this.settings.appPassword, + scope: _this.settings.endpoint.refreshScope + } + }; + _this.addUserAgent(opt); + request(opt, function (err, response, body) { + if (!err) { + if (body && response.statusCode < 300) { + var oauthResponse = JSON.parse(body); + _this.accessToken = oauthResponse.access_token; + _this.accessTokenExpires = new Date().getTime() + ((oauthResponse.expires_in - 300) * 1000); + _this.refreshingToken = undefined; + resolve(_this.accessToken); + } + else { + reject(new Error('Refresh access token failed with status code: ' + response.statusCode)); + } + } + else { + reject(err); + } + }); + }); + } + this.refreshingToken.then(function (token) { return cb(null, token); }, function (err) { return cb(err, null); }); }; ChatConnector.prototype.getAccessToken = function (cb) { var _this = this; diff --git a/Node/core/src/bots/ChatConnector.ts b/Node/core/src/bots/ChatConnector.ts index c56c9d935b..ca62aca687 100644 --- a/Node/core/src/bots/ChatConnector.ts +++ b/Node/core/src/bots/ChatConnector.ts @@ -44,6 +44,7 @@ import * as url from 'url'; import * as http from 'http'; import * as jwt from 'jsonwebtoken'; import * as zlib from 'zlib'; +import * as Promise from 'promise'; import urlJoin = require('url-join'); var pjson = require('../../package.json'); @@ -98,6 +99,7 @@ export class ChatConnector implements IConnector, IBotStorage { private accessTokenExpires: number; private botConnectorOpenIdMetadata: OpenIdMetadata; private emulatorOpenIdMetadata: OpenIdMetadata; + private refreshingToken: Promise.IThenable; constructor(protected settings: IChatConnectorSettings = {}) { if (!this.settings.endpoint) { @@ -693,33 +695,40 @@ export class ChatConnector implements IConnector, IBotStorage { } private refreshAccessToken(cb: (err: Error, accessToken: string) => void): void { - var opt: request.Options = { - method: 'POST', - url: this.settings.endpoint.refreshEndpoint, - form: { - grant_type: 'client_credentials', - client_id: this.settings.appId, - client_secret: this.settings.appPassword, - scope: this.settings.endpoint.refreshScope - } - }; - this.addUserAgent(opt); - request(opt, (err, response, body) => { - if (!err) { - if (body && response.statusCode < 300) { - // Subtract 5 minutes from expires_in so they'll we'll get a - // new token before it expires. - var oauthResponse = JSON.parse(body); - this.accessToken = oauthResponse.access_token; - this.accessTokenExpires = new Date().getTime() + ((oauthResponse.expires_in - 300) * 1000); - cb(null, this.accessToken); - } else { - cb(new Error('Refresh access token failed with status code: ' + response.statusCode), null); - } - } else { - cb(err, null); - } - }); + // Get token only on first access. Other callers will block while waiting for token. + if (!this.refreshingToken) { + this.refreshingToken = new Promise((resolve, reject) => { + var opt: request.Options = { + method: 'POST', + url: this.settings.endpoint.refreshEndpoint, + form: { + grant_type: 'client_credentials', + client_id: this.settings.appId, + client_secret: this.settings.appPassword, + scope: this.settings.endpoint.refreshScope + } + }; + this.addUserAgent(opt); + request(opt, (err, response, body) => { + if (!err) { + if (body && response.statusCode < 300) { + // Subtract 5 minutes from expires_in so they'll we'll get a + // new token before it expires. + var oauthResponse = JSON.parse(body); + this.accessToken = oauthResponse.access_token; + this.accessTokenExpires = new Date().getTime() + ((oauthResponse.expires_in - 300) * 1000); + this.refreshingToken = undefined; + resolve(this.accessToken); + } else { + reject(new Error('Refresh access token failed with status code: ' + response.statusCode)); + } + } else { + reject(err); + } + }); + }) + } + this.refreshingToken.then((token) => cb(null, token), (err) => cb(err, null)); } public getAccessToken(cb: (err: Error, accessToken: string) => void): void {