tx.js 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /* eslint-disable complexity, max-statements */
  2. var http = require('./http');
  3. var util = require('./util');
  4. var Q = require('q');
  5. var AuthSdkError = require('./errors/AuthSdkError');
  6. var AuthPollStopError = require('./errors/AuthPollStopError');
  7. var config = require('./config');
  8. function addStateToken(res, options) {
  9. var builtArgs = {};
  10. util.extend(builtArgs, options);
  11. // Add the stateToken if one isn't passed and we have one
  12. if (!builtArgs.stateToken && res.stateToken) {
  13. builtArgs.stateToken = res.stateToken;
  14. }
  15. return builtArgs;
  16. }
  17. function getStateToken(res) {
  18. return addStateToken(res);
  19. }
  20. function transactionStatus(sdk, args) {
  21. args = addStateToken(sdk, args);
  22. return http.post(sdk, sdk.options.url + '/api/v1/authn', args);
  23. }
  24. function resumeTransaction(sdk, args) {
  25. if (!args || !args.stateToken) {
  26. var stateToken = sdk.tx.exists._getCookie(config.STATE_TOKEN_COOKIE_NAME);
  27. if (stateToken) {
  28. args = {
  29. stateToken: stateToken
  30. };
  31. } else {
  32. return Q.reject(new AuthSdkError('No transaction to resume'));
  33. }
  34. }
  35. return sdk.tx.status(args)
  36. .then(function(res) {
  37. return new AuthTransaction(sdk, res);
  38. });
  39. }
  40. function transactionExists(sdk) {
  41. // We have a cookie state token
  42. return !!sdk.tx.exists._getCookie(config.STATE_TOKEN_COOKIE_NAME);
  43. }
  44. function postToTransaction(sdk, url, args, options) {
  45. return http.post(sdk, url, args, options)
  46. .then(function(res) {
  47. return new AuthTransaction(sdk, res);
  48. });
  49. }
  50. function getPollFn(sdk, res, ref) {
  51. return function (options) {
  52. var delay;
  53. var rememberDevice;
  54. var autoPush;
  55. if (util.isNumber(options)) {
  56. delay = options;
  57. } else if (util.isObject(options)) {
  58. delay = options.delay;
  59. rememberDevice = options.rememberDevice;
  60. autoPush = options.autoPush;
  61. }
  62. if (!delay && delay !== 0) {
  63. delay = config.DEFAULT_POLLING_DELAY;
  64. }
  65. // Get the poll function
  66. var pollLink = util.getLink(res, 'next', 'poll');
  67. function pollFn() {
  68. var opts = {};
  69. if (typeof autoPush === 'function') {
  70. try {
  71. opts.autoPush = !!autoPush();
  72. }
  73. catch (e) {
  74. return Q.reject(new AuthSdkError('AutoPush resulted in an error.'));
  75. }
  76. }
  77. else if (autoPush !== undefined && autoPush !== null) {
  78. opts.autoPush = !!autoPush;
  79. }
  80. if (typeof rememberDevice === 'function') {
  81. try {
  82. opts.rememberDevice = !!rememberDevice();
  83. }
  84. catch (e) {
  85. return Q.reject(new AuthSdkError('RememberDevice resulted in an error.'));
  86. }
  87. }
  88. else if (rememberDevice !== undefined && rememberDevice !== null) {
  89. opts.rememberDevice = !!rememberDevice;
  90. }
  91. var href = pollLink.href + util.toQueryParams(opts);
  92. return http.post(sdk, href, getStateToken(res), {
  93. saveAuthnState: false
  94. });
  95. }
  96. ref.isPolling = true;
  97. var retryCount = 0;
  98. var recursivePoll = function () {
  99. // If the poll was manually stopped during the delay
  100. if (!ref.isPolling) {
  101. return Q.reject(new AuthPollStopError());
  102. }
  103. return pollFn()
  104. .then(function (pollRes) {
  105. // Reset our retry counter on success
  106. retryCount = 0;
  107. // If we're still waiting
  108. if (pollRes.factorResult && pollRes.factorResult === 'WAITING') {
  109. // If the poll was manually stopped while the pollFn was called
  110. if (!ref.isPolling) {
  111. throw new AuthPollStopError();
  112. }
  113. // Continue poll
  114. return Q.delay(delay)
  115. .then(recursivePoll);
  116. } else {
  117. // Any non-waiting result, even if polling was stopped
  118. // during a request, will return
  119. ref.isPolling = false;
  120. return new AuthTransaction(sdk, pollRes);
  121. }
  122. })
  123. .fail(function(err) {
  124. // Exponential backoff, up to 16 seconds
  125. if (err.xhr &&
  126. (err.xhr.status === 0 || err.xhr.status === 429) &&
  127. retryCount <= 4) {
  128. var delayLength = Math.pow(2, retryCount) * 1000;
  129. retryCount++;
  130. return Q.delay(delayLength)
  131. .then(recursivePoll);
  132. }
  133. throw err;
  134. });
  135. };
  136. return recursivePoll()
  137. .fail(function(err) {
  138. ref.isPolling = false;
  139. throw err;
  140. });
  141. };
  142. }
  143. function link2fn(sdk, res, obj, link, ref) {
  144. if (Array.isArray(link)) {
  145. return function(name, opts) {
  146. if (!name) {
  147. throw new AuthSdkError('Must provide a link name');
  148. }
  149. var lk = util.find(link, {name: name});
  150. if (!lk) {
  151. throw new AuthSdkError('No link found for that name');
  152. }
  153. return link2fn(sdk, res, obj, lk, ref)(opts);
  154. };
  155. } else if (link.hints &&
  156. link.hints.allow &&
  157. link.hints.allow.length === 1) {
  158. var method = link.hints.allow[0];
  159. switch (method) {
  160. case 'GET':
  161. return function() {
  162. return http.get(sdk, link.href);
  163. };
  164. case 'POST':
  165. return function(opts) {
  166. if (ref && ref.isPolling) {
  167. ref.isPolling = false;
  168. }
  169. var data = addStateToken(res, opts);
  170. if (res.status === 'MFA_ENROLL') {
  171. // Add factorType and provider
  172. util.extend(data, {
  173. factorType: obj.factorType,
  174. provider: obj.provider
  175. });
  176. }
  177. var params = {};
  178. var autoPush = data.autoPush;
  179. if (autoPush !== undefined) {
  180. if (typeof autoPush === 'function') {
  181. try {
  182. params.autoPush = !!autoPush();
  183. }
  184. catch (e) {
  185. return Q.reject(new AuthSdkError('AutoPush resulted in an error.'));
  186. }
  187. }
  188. else if (autoPush !== null) {
  189. params.autoPush = !!autoPush;
  190. }
  191. data = util.omit(data, 'autoPush');
  192. }
  193. var rememberDevice = data.rememberDevice;
  194. if (rememberDevice !== undefined) {
  195. if (typeof rememberDevice === 'function') {
  196. try {
  197. params.rememberDevice = !!rememberDevice();
  198. }
  199. catch (e) {
  200. return Q.reject(new AuthSdkError('RememberDevice resulted in an error.'));
  201. }
  202. }
  203. else if (rememberDevice !== null) {
  204. params.rememberDevice = !!rememberDevice;
  205. }
  206. data = util.omit(data, 'rememberDevice');
  207. } else if (data.profile &&
  208. data.profile.updatePhone !== undefined) {
  209. if (data.profile.updatePhone) {
  210. params.updatePhone = true;
  211. }
  212. data.profile = util.omit(data.profile, 'updatePhone');
  213. }
  214. var href = link.href + util.toQueryParams(params);
  215. return postToTransaction(sdk, href, data);
  216. };
  217. }
  218. }
  219. }
  220. function links2fns(sdk, res, obj, ref) {
  221. var fns = {};
  222. for (var linkName in obj._links) {
  223. if (!obj._links.hasOwnProperty(linkName)) {
  224. continue;
  225. }
  226. var link = obj._links[linkName];
  227. if (linkName === 'next') {
  228. linkName = link.name;
  229. }
  230. if (link.type) {
  231. fns[linkName] = link;
  232. continue;
  233. }
  234. switch (linkName) {
  235. // poll is only found at the transaction
  236. // level, so we don't need to pass the link
  237. case 'poll':
  238. fns.poll = getPollFn(sdk, res, ref);
  239. break;
  240. default:
  241. var fn = link2fn(sdk, res, obj, link, ref);
  242. if (fn) {
  243. fns[linkName] = fn;
  244. }
  245. }
  246. }
  247. return fns;
  248. }
  249. function flattenEmbedded(sdk, res, obj, ref) {
  250. obj = obj || res;
  251. obj = util.clone(obj);
  252. if (Array.isArray(obj)) {
  253. var objArr = [];
  254. for (var o = 0, ol = obj.length; o < ol; o++) {
  255. objArr.push(flattenEmbedded(sdk, res, obj[o], ref));
  256. }
  257. return objArr;
  258. }
  259. var embedded = obj._embedded || {};
  260. for (var key in embedded) {
  261. if (!embedded.hasOwnProperty(key)) {
  262. continue;
  263. }
  264. // Flatten any nested _embedded objects
  265. if (util.isObject(embedded[key]) || Array.isArray(embedded[key])) {
  266. embedded[key] = flattenEmbedded(sdk, res, embedded[key], ref);
  267. }
  268. }
  269. // Convert any links on the embedded object
  270. var fns = links2fns(sdk, res, obj, ref);
  271. util.extend(embedded, fns);
  272. obj = util.omit(obj, '_embedded', '_links');
  273. util.extend(obj, embedded);
  274. return obj;
  275. }
  276. function AuthTransaction(sdk, res) {
  277. if (res) {
  278. this.data = res;
  279. util.extend(this, flattenEmbedded(sdk, res, res, {}));
  280. delete this.stateToken;
  281. // RECOVERY_CHALLENGE has some responses without _links.
  282. // Without _links, we emulate cancel to make it intuitive
  283. // to return to the starting state. We may remove this
  284. // when OKTA-75434 is resolved
  285. if (res.status === 'RECOVERY_CHALLENGE' && !res._links) {
  286. this.cancel = function() {
  287. return new Q(new AuthTransaction(sdk));
  288. };
  289. }
  290. }
  291. }
  292. module.exports = {
  293. transactionStatus: transactionStatus,
  294. resumeTransaction: resumeTransaction,
  295. transactionExists: transactionExists,
  296. postToTransaction: postToTransaction
  297. };