oauth2client.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /*
  2. * @copyright
  3. * Copyright © Microsoft Open Technologies, Inc.
  4. *
  5. * All Rights Reserved
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http: *www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
  14. * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
  15. * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
  16. * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
  17. *
  18. * See the Apache License, Version 2.0 for the specific language
  19. * governing permissions and limitations under the License.
  20. */
  21. 'use strict';
  22. var _ = require('underscore');
  23. require('date-utils'); // Adds a number of convenience methods to the builtin Date object.
  24. var querystring = require('querystring');
  25. var uuid = require('uuid');
  26. var request = require('request');
  27. var url = require('url');
  28. var async = require('async');
  29. var constants = require('./constants');
  30. var Logger = require('./log').Logger;
  31. var util = require('./util');
  32. var OAuth2Parameters = constants.OAuth2.Parameters;
  33. var OAuth2ResponseParameters = constants.OAuth2.ResponseParameters;
  34. var DeviceCodeResponseParameters = constants.OAuth2.DeviceCodeResponseParameters;
  35. var IdTokenMap = constants.OAuth2.IdTokenMap;
  36. var TokenResponseFields = constants.TokenResponseFields;
  37. var UserCodeResponseFields = constants.UserCodeResponseFields;
  38. var IdTokenFields = constants.IdTokenFields;
  39. var TOKEN_RESPONSE_MAP = {};
  40. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.TOKEN_TYPE] = TokenResponseFields.TOKEN_TYPE;
  41. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.ACCESS_TOKEN] = TokenResponseFields.ACCESS_TOKEN;
  42. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.REFRESH_TOKEN] = TokenResponseFields.REFRESH_TOKEN;
  43. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.CREATED_ON] = TokenResponseFields.CREATED_ON;
  44. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.EXPIRES_ON] = TokenResponseFields.EXPIRES_ON;
  45. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.EXPIRES_IN] = TokenResponseFields.EXPIRES_IN;
  46. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.RESOURCE] = TokenResponseFields.RESOURCE;
  47. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.ERROR] = TokenResponseFields.ERROR;
  48. TOKEN_RESPONSE_MAP[OAuth2ResponseParameters.ERROR_DESCRIPTION] = TokenResponseFields.ERROR_DESCRIPTION;
  49. var DEVICE_CODE_RESPONSE_MAP = {};
  50. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.DEVICE_CODE] = UserCodeResponseFields.DEVICE_CODE;
  51. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.USER_CODE] = UserCodeResponseFields.USER_CODE;
  52. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.VERIFICATION_URL] = UserCodeResponseFields.VERIFICATION_URL;
  53. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.INTERVAL] = UserCodeResponseFields.INTERVAL;
  54. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.EXPIRES_IN] = UserCodeResponseFields.EXPIRES_IN;
  55. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.MESSAGE] = UserCodeResponseFields.MESSAGE;
  56. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.ERROR] = UserCodeResponseFields.ERROR;
  57. DEVICE_CODE_RESPONSE_MAP[DeviceCodeResponseParameters.ERROR_DESCRIPTION] = UserCodeResponseFields.ERROR_DESCRIPTION;
  58. /**
  59. * Constructs an instances of OAuth2Client
  60. * @constructor
  61. * @private
  62. * @param {object} callContext Contains any context information that applies to the request.
  63. * @param {string|url} authority An url that points to an authority.
  64. */
  65. function OAuth2Client(callContext, authority) {
  66. this._tokenEndpoint = authority.tokenEndpoint;
  67. this._deviceCodeEndpoint = authority.deviceCodeEndpoint;
  68. this._log = new Logger('OAuth2Client', callContext._logContext);
  69. this._callContext = callContext;
  70. this._cancelPollingRequest = false;
  71. }
  72. /**
  73. * Constructs an OAuth 2.0 token request url.
  74. * @private
  75. * @return {URL}
  76. */
  77. OAuth2Client.prototype._createTokenUrl = function () {
  78. var tokenUrl = url.parse(this._tokenEndpoint);
  79. var parameters = {};
  80. parameters[OAuth2Parameters.AAD_API_VERSION] = '1.0';
  81. tokenUrl.search = querystring.stringify(parameters);
  82. return tokenUrl;
  83. };
  84. /**
  85. * Constructs the user code info request url.
  86. * @private
  87. * @return {URL}
  88. */
  89. OAuth2Client.prototype._createDeviceCodeUrl = function () {
  90. var deviceCodeUrl = url.parse(this._deviceCodeEndpoint);
  91. var parameters = {};
  92. parameters[OAuth2Parameters.AAD_API_VERSION] = '1.0';
  93. deviceCodeUrl.search = querystring.stringify(parameters);
  94. return deviceCodeUrl;
  95. };
  96. /**
  97. * @private
  98. * @param {object} obj An object in which integer values may reside.
  99. * @param {array} keys An array of strings that specify keys in which integers may need parsing.
  100. */
  101. OAuth2Client.prototype._parseOptionalInts = function (obj, keys) {
  102. var self = this;
  103. keys.forEach(function(element) {
  104. if (_.has(obj, element)) {
  105. obj[element] = parseInt(obj[element], 10);
  106. if (isNaN(obj[element])) {
  107. throw self._log.createError(element + ' could not be parsed as an int.');
  108. }
  109. }
  110. });
  111. };
  112. /**
  113. * Parses a JWS encoded JWT into it's three parts.
  114. * @param {string} jwtToken The token to parse.
  115. * @return {object} The three JWS parts, header, JWSPayload, and JWSSig, or undefined.
  116. */
  117. OAuth2Client.prototype._crackJwt = function(jwtToken) {
  118. var idTokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/;
  119. var matches = idTokenPartsRegex.exec(jwtToken);
  120. if (!matches || matches.length < 4) {
  121. this._log.warn('The returned id_token is not parseable.');
  122. return;
  123. }
  124. var crackedToken = {
  125. header : matches[1],
  126. JWSPayload : matches[2],
  127. JWSSig : matches[3]
  128. };
  129. return crackedToken;
  130. };
  131. /**
  132. * Finds the value that should be used as the userId value.
  133. * @param {object} idToken The id token that parsed.
  134. * @returns {object} An object with a userId field and maybe a userIdIsDisplayable field.
  135. */
  136. OAuth2Client.prototype._getUserId = function(idToken) {
  137. var userId;
  138. var isDisplayable;
  139. if (idToken.upn) {
  140. userId = idToken.upn;
  141. isDisplayable = true;
  142. } else if (idToken.email) {
  143. userId = idToken.email;
  144. isDisplayable = true;
  145. } else if (idToken.sub) {
  146. userId = idToken.sub;
  147. }
  148. if (!userId) {
  149. // generate a random GUID.
  150. userId = uuid.v4();
  151. }
  152. var userIdVals = {};
  153. userIdVals[IdTokenFields.USER_ID] = userId;
  154. if (isDisplayable) {
  155. userIdVals[IdTokenFields.IS_USER_ID_DISPLAYABLE] = true;
  156. }
  157. return userIdVals;
  158. };
  159. function mapFields(inObj, outObj, map) {
  160. for (var key in inObj) {
  161. if (map[key]) {
  162. var mappedKey = map[key];
  163. outObj[mappedKey] = inObj[key];
  164. }
  165. }
  166. }
  167. /**
  168. * Given a decoded id token off the wire, this function extracts the values that
  169. * ADAL commonly returns to callers and translates the names to more user
  170. * friendly names.
  171. * @param {Object} idToken A decoded id token.
  172. * @return {Object} The set of extracted values with their new names.
  173. */
  174. OAuth2Client.prototype._extractIdTokenValues = function(idToken) {
  175. var extractedValues = {};
  176. _.extend(extractedValues, this._getUserId(idToken));
  177. mapFields(idToken, extractedValues, IdTokenMap);
  178. return extractedValues;
  179. };
  180. /**
  181. * Parses the value of the id_token OAuth 2 Reponse.
  182. * @param {string} encodedIdToken An unencrypted JWT token.
  183. * @return {object} returns the decoded id_token or undefined.
  184. */
  185. OAuth2Client.prototype._parseIdToken = function(encodedIdToken) {
  186. var crackedToken = this._crackJwt(encodedIdToken);
  187. if (!crackedToken) {
  188. return;
  189. }
  190. var idToken;
  191. try {
  192. var base64IdToken = crackedToken.JWSPayload;
  193. var base64Decoded = util.base64DecodeStringUrlSafe(base64IdToken);
  194. if (!base64Decoded) {
  195. this._log.warn('The returned id_token could not be base64 url safe decoded.');
  196. return;
  197. }
  198. idToken = JSON.parse(base64Decoded);
  199. } catch (err) {
  200. this._log.warn('the returned id_token could not be decoded');
  201. this._log.warn('The returned id_token could not be decoded: ' + err.stack, true);
  202. return;
  203. }
  204. return this._extractIdTokenValues(idToken);
  205. };
  206. /**
  207. * Validates the response returned from an OAuth 2.0 token request.
  208. * @private
  209. * @param {string} body The response as a string encoded JSON object.
  210. * @return {object} The parsed response.
  211. */
  212. OAuth2Client.prototype._validateTokenResponse = function(body) {
  213. var wireResponse;
  214. var tokenResponse = {};
  215. try {
  216. wireResponse = JSON.parse(body);
  217. } catch(e) {
  218. throw new Error('The token response returned from the server is unparseable as JSON');
  219. }
  220. var intKeys = [
  221. OAuth2ResponseParameters.EXPIRES_ON,
  222. OAuth2ResponseParameters.EXPIRES_IN,
  223. OAuth2ResponseParameters.CREATED_ON
  224. ];
  225. this._parseOptionalInts(wireResponse, intKeys);
  226. if (wireResponse[OAuth2ResponseParameters.EXPIRES_IN]) {
  227. var expiresIn = wireResponse[OAuth2ResponseParameters.EXPIRES_IN];
  228. var now = new Date();
  229. wireResponse[OAuth2ResponseParameters.EXPIRES_ON] = now.add( { seconds : expiresIn });
  230. }
  231. if (wireResponse[OAuth2ResponseParameters.CREATED_ON]) {
  232. var tempDate = new Date();
  233. var createdOn = wireResponse[OAuth2ResponseParameters.CREATED_ON];
  234. tempDate.setTime(createdOn);
  235. wireResponse[OAuth2ResponseParameters.CREATED_ON] = tempDate;
  236. }
  237. if (!wireResponse[OAuth2ResponseParameters.TOKEN_TYPE]) {
  238. throw this._log.createError('wireResponse is missing token_type');
  239. }
  240. if (!wireResponse[OAuth2ResponseParameters.ACCESS_TOKEN]) {
  241. throw this._log.createError('wireResponse missing access_token');
  242. }
  243. mapFields(wireResponse, tokenResponse, TOKEN_RESPONSE_MAP);
  244. if (wireResponse[OAuth2ResponseParameters.ID_TOKEN]) {
  245. var idToken = this._parseIdToken(wireResponse[OAuth2ResponseParameters.ID_TOKEN]);
  246. if (idToken) {
  247. _.extend(tokenResponse, idToken);
  248. }
  249. }
  250. return tokenResponse;
  251. };
  252. /**
  253. * Validates the response returned from an OAuth 2.0 device code request.
  254. * @private
  255. * @param {string} body The response as a string encoded JSON object.
  256. * @return {object} The parsed response.
  257. */
  258. OAuth2Client.prototype._validateDeviceCodeResponse = function(body) {
  259. var wireResponse;
  260. var deviceCodeResponse = {};
  261. try {
  262. wireResponse = JSON.parse(body);
  263. } catch(e) {
  264. throw new Error('The device code response returned from the server is unparseable as JSON.');
  265. }
  266. var intKeys = [
  267. DeviceCodeResponseParameters.EXPIRES_IN,
  268. DeviceCodeResponseParameters.INTERVAL
  269. ];
  270. this._parseOptionalInts(wireResponse, intKeys);
  271. if (!wireResponse[DeviceCodeResponseParameters.EXPIRES_IN]){
  272. throw this._log.createError('wireResponse is missing expires_in');
  273. }
  274. if (!wireResponse[DeviceCodeResponseParameters.DEVICE_CODE]) {
  275. throw this._log.createError('wireResponse is missing device code');
  276. }
  277. if (!wireResponse[DeviceCodeResponseParameters.USER_CODE]) {
  278. throw this._log.createError('wireResponse is missing user code');
  279. }
  280. mapFields(wireResponse, deviceCodeResponse, DEVICE_CODE_RESPONSE_MAP);
  281. return deviceCodeResponse;
  282. };
  283. /**
  284. * @private
  285. * @param {string} body The body of a http token response.
  286. */
  287. OAuth2Client.prototype._handlePollingResponse = function(body) {
  288. //handle token error response
  289. var tokenResponse = this._handlePollingRequestErrorResponse(body);
  290. if (_.isEmpty(tokenResponse)){
  291. tokenResponse = this._validateTokenResponse(body);
  292. }
  293. return tokenResponse;
  294. };
  295. /**
  296. * @private
  297. * @param {string} body The body of a http token response.
  298. */
  299. OAuth2Client.prototype._handlePollingRequestErrorResponse = function(body) {
  300. var wireResponse;
  301. var tokenResponse = {};
  302. try {
  303. wireResponse = JSON.parse(body);
  304. } catch (e) {
  305. throw new Error ('The token response returned from the server is unparsable as JSON');
  306. }
  307. if (wireResponse[OAuth2ResponseParameters.ERROR]) {
  308. mapFields(wireResponse, tokenResponse, TOKEN_RESPONSE_MAP);
  309. }
  310. return tokenResponse;
  311. };
  312. /**
  313. * @private
  314. * @param {object} response An http response object.
  315. * @param {string} body The body of a http token response.
  316. * @param {OAuth2Client.GetTokenCallback} callback A call back function. The body parameter is the body parameter passed
  317. * into this function.
  318. */
  319. OAuth2Client.prototype._handleGetTokenResponse = function(response, body, callback) {
  320. var tokenResponse;
  321. try {
  322. tokenResponse = this._validateTokenResponse(body);
  323. } catch (e) {
  324. this._log.error('Error validating get token response', e, true);
  325. callback(e);
  326. return;
  327. }
  328. callback(null, tokenResponse);
  329. };
  330. OAuth2Client.prototype._handleGetDeviceCodeResponse = function(response, body, callback) {
  331. var deviceCodeResponse;
  332. try {
  333. deviceCodeResponse = this._validateDeviceCodeResponse(body);
  334. } catch (e) {
  335. this._log.error('Error validating get user code response', e, true);
  336. callback(e);
  337. return;
  338. }
  339. callback(null, deviceCodeResponse);
  340. };
  341. OAuth2Client.prototype._getTokenWithPolling = function (postOptions, callback) {
  342. var self = this;
  343. if (self._cancelPollingRequest === true) {
  344. callback(null, new Error('Polling_Request_Cancelled'));
  345. return;
  346. }
  347. request.post(postOptions, util.createRequestHandler('Get Token', this._log, function(response, body) {
  348. //error response callback, for error response, it's already parsed as Json.
  349. if (body && body.hasOwnProperty(TokenResponseFields.ERROR) && body[TokenResponseFields.ERROR] === 'authorization_pending') {
  350. callback(new Error(body[TokenResponseFields.ERROR]), body);
  351. }
  352. else {
  353. callback(null, body);
  354. }
  355. },
  356. // success response callback
  357. function (response, body) {
  358. var tokenResponse;
  359. try {
  360. tokenResponse = self._handlePollingResponse(body);
  361. } catch (e) {
  362. self._log.error('Error validating get token response', e, true);
  363. callback(null, e);
  364. return;
  365. }
  366. callback(null, tokenResponse);
  367. })
  368. );
  369. };
  370. OAuth2Client.prototype._createPostOption = function (postUrl, urlEncodedRequestForm) {
  371. var postOptions = util.createRequestOptions(
  372. this,
  373. {
  374. 'url' : url.format(postUrl),
  375. body : urlEncodedRequestForm,
  376. headers: {
  377. 'Content-Type': 'application/x-www-form-urlencoded'
  378. },
  379. followRedirect : false,
  380. encoding : 'utf8'
  381. }
  382. );
  383. return postOptions;
  384. };
  385. /**
  386. * @callback GetTokenCallback
  387. * @memberOf OAuth2Client
  388. * @param {Error} [error] In case of an error this will hold the associated Error object.
  389. * @param {TokenResponse} tokenResponse Contains the parsed result of a get token request.
  390. */
  391. /**
  392. * @param {object} oauthParameters An object whose keys come from
  393. * Constants.OAuth2.Parameters
  394. * @param {OAuth2Client.GetTokenCallback} callback The callback function.
  395. */
  396. OAuth2Client.prototype.getToken = function(oauthParameters, callback) {
  397. var self = this;
  398. var tokenUrl = self._createTokenUrl();
  399. var urlEncodedTokenRequestForm = querystring.stringify(oauthParameters);
  400. var postOptions = self._createPostOption(tokenUrl, urlEncodedTokenRequestForm);
  401. request.post(postOptions, util.createRequestHandler('Get Token', this._log, callback,
  402. function (response, body) {
  403. self._handleGetTokenResponse(response, body, callback);
  404. })
  405. );
  406. };
  407. /**
  408. * @param {object} oauthParameters An object whose keys come from
  409. * Constants.OAuth2.Parameters
  410. * @param {integer} refresh_interval The interval for polling request.
  411. * @param {integer} exipres_in The timeout for polling request.
  412. * @param {OAuth2Client.GetTokenCallback} callback The callback function.
  413. */
  414. OAuth2Client.prototype.getTokenWithPolling = function(oauthParameters, refresh_interval, expires_in, callback){
  415. var self = this;
  416. var maxTimesForRetry = Math.floor(expires_in / refresh_interval);
  417. var tokenUrl = self._createTokenUrl();
  418. var urlEncodedTokenRequestForm = querystring.stringify(oauthParameters);
  419. var postOptions = self._createPostOption(tokenUrl, urlEncodedTokenRequestForm);
  420. var optionsForRetry = {times: maxTimesForRetry, interval: refresh_interval * 1000};
  421. async.retry(optionsForRetry, function(retryCallback, response) {
  422. self._getTokenWithPolling(postOptions, retryCallback);
  423. }, function(err, response) {
  424. if (response && response instanceof Error) {
  425. callback(response);
  426. return;
  427. }
  428. else if (response && response.hasOwnProperty(DeviceCodeResponseParameters.ERROR)) {
  429. callback(response);
  430. return;
  431. }
  432. callback(err, response);
  433. });
  434. };
  435. OAuth2Client.prototype.getUserCodeInfo = function(oauthParameters, callback) {
  436. // for now make it as a post request
  437. var self = this;
  438. var deviceCodeUrl = self._createDeviceCodeUrl();
  439. var urlEncodedDeviceCodeRequestForm = querystring.stringify(oauthParameters);
  440. var postOptions = self._createPostOption(deviceCodeUrl, urlEncodedDeviceCodeRequestForm);
  441. request.post(postOptions, util.createRequestHandler('Get Device Code ', this._log, callback,
  442. function (response, body) {
  443. self._handleGetDeviceCodeResponse(response, body, callback);
  444. })
  445. );
  446. };
  447. /**
  448. * Cancel the polling request made for acquiring token by device code.
  449. */
  450. OAuth2Client.prototype.cancelPollingRequest = function() {
  451. this._cancelPollingRequest = true;
  452. };
  453. module.exports = OAuth2Client;