| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- /* eslint-disable complexity, max-statements */
- var http = require('./http');
- var util = require('./util');
- var Q = require('q');
- var AuthSdkError = require('./errors/AuthSdkError');
- var AuthPollStopError = require('./errors/AuthPollStopError');
- var config = require('./config');
-
- function addStateToken(res, options) {
- var builtArgs = {};
- util.extend(builtArgs, options);
-
- // Add the stateToken if one isn't passed and we have one
- if (!builtArgs.stateToken && res.stateToken) {
- builtArgs.stateToken = res.stateToken;
- }
-
- return builtArgs;
- }
-
- function getStateToken(res) {
- return addStateToken(res);
- }
-
- function transactionStatus(sdk, args) {
- args = addStateToken(sdk, args);
- return http.post(sdk, sdk.options.url + '/api/v1/authn', args);
- }
-
- function resumeTransaction(sdk, args) {
- if (!args || !args.stateToken) {
- var stateToken = sdk.tx.exists._getCookie(config.STATE_TOKEN_COOKIE_NAME);
- if (stateToken) {
- args = {
- stateToken: stateToken
- };
- } else {
- return Q.reject(new AuthSdkError('No transaction to resume'));
- }
- }
- return sdk.tx.status(args)
- .then(function(res) {
- return new AuthTransaction(sdk, res);
- });
- }
-
- function transactionExists(sdk) {
- // We have a cookie state token
- return !!sdk.tx.exists._getCookie(config.STATE_TOKEN_COOKIE_NAME);
- }
-
- function postToTransaction(sdk, url, args, options) {
- return http.post(sdk, url, args, options)
- .then(function(res) {
- return new AuthTransaction(sdk, res);
- });
- }
-
- function getPollFn(sdk, res, ref) {
- return function (options) {
- var delay;
- var rememberDevice;
- var autoPush;
-
- if (util.isNumber(options)) {
- delay = options;
- } else if (util.isObject(options)) {
- delay = options.delay;
- rememberDevice = options.rememberDevice;
- autoPush = options.autoPush;
- }
-
- if (!delay && delay !== 0) {
- delay = config.DEFAULT_POLLING_DELAY;
- }
-
- // Get the poll function
- var pollLink = util.getLink(res, 'next', 'poll');
- function pollFn() {
- var opts = {};
- if (typeof autoPush === 'function') {
- try {
- opts.autoPush = !!autoPush();
- }
- catch (e) {
- return Q.reject(new AuthSdkError('AutoPush resulted in an error.'));
- }
- }
- else if (autoPush !== undefined && autoPush !== null) {
- opts.autoPush = !!autoPush;
- }
- if (typeof rememberDevice === 'function') {
- try {
- opts.rememberDevice = !!rememberDevice();
- }
- catch (e) {
- return Q.reject(new AuthSdkError('RememberDevice resulted in an error.'));
- }
- }
- else if (rememberDevice !== undefined && rememberDevice !== null) {
- opts.rememberDevice = !!rememberDevice;
- }
-
- var href = pollLink.href + util.toQueryParams(opts);
- return http.post(sdk, href, getStateToken(res), {
- saveAuthnState: false
- });
- }
-
- ref.isPolling = true;
-
- var retryCount = 0;
- var recursivePoll = function () {
- // If the poll was manually stopped during the delay
- if (!ref.isPolling) {
- return Q.reject(new AuthPollStopError());
- }
- return pollFn()
- .then(function (pollRes) {
- // Reset our retry counter on success
- retryCount = 0;
-
- // If we're still waiting
- if (pollRes.factorResult && pollRes.factorResult === 'WAITING') {
-
- // If the poll was manually stopped while the pollFn was called
- if (!ref.isPolling) {
- throw new AuthPollStopError();
- }
-
- // Continue poll
- return Q.delay(delay)
- .then(recursivePoll);
-
- } else {
- // Any non-waiting result, even if polling was stopped
- // during a request, will return
- ref.isPolling = false;
- return new AuthTransaction(sdk, pollRes);
- }
- })
- .fail(function(err) {
- // Exponential backoff, up to 16 seconds
- if (err.xhr &&
- (err.xhr.status === 0 || err.xhr.status === 429) &&
- retryCount <= 4) {
- var delayLength = Math.pow(2, retryCount) * 1000;
- retryCount++;
- return Q.delay(delayLength)
- .then(recursivePoll);
- }
- throw err;
- });
- };
- return recursivePoll()
- .fail(function(err) {
- ref.isPolling = false;
- throw err;
- });
- };
- }
-
- function link2fn(sdk, res, obj, link, ref) {
- if (Array.isArray(link)) {
- return function(name, opts) {
- if (!name) {
- throw new AuthSdkError('Must provide a link name');
- }
-
- var lk = util.find(link, {name: name});
- if (!lk) {
- throw new AuthSdkError('No link found for that name');
- }
-
- return link2fn(sdk, res, obj, lk, ref)(opts);
- };
-
- } else if (link.hints &&
- link.hints.allow &&
- link.hints.allow.length === 1) {
- var method = link.hints.allow[0];
- switch (method) {
-
- case 'GET':
- return function() {
- return http.get(sdk, link.href);
- };
-
- case 'POST':
- return function(opts) {
- if (ref && ref.isPolling) {
- ref.isPolling = false;
- }
-
- var data = addStateToken(res, opts);
-
- if (res.status === 'MFA_ENROLL') {
- // Add factorType and provider
- util.extend(data, {
- factorType: obj.factorType,
- provider: obj.provider
- });
- }
-
- var params = {};
- var autoPush = data.autoPush;
- if (autoPush !== undefined) {
- if (typeof autoPush === 'function') {
- try {
- params.autoPush = !!autoPush();
- }
- catch (e) {
- return Q.reject(new AuthSdkError('AutoPush resulted in an error.'));
- }
- }
- else if (autoPush !== null) {
- params.autoPush = !!autoPush;
- }
- data = util.omit(data, 'autoPush');
- }
-
- var rememberDevice = data.rememberDevice;
- if (rememberDevice !== undefined) {
- if (typeof rememberDevice === 'function') {
- try {
- params.rememberDevice = !!rememberDevice();
- }
- catch (e) {
- return Q.reject(new AuthSdkError('RememberDevice resulted in an error.'));
- }
- }
- else if (rememberDevice !== null) {
- params.rememberDevice = !!rememberDevice;
- }
- data = util.omit(data, 'rememberDevice');
-
- } else if (data.profile &&
- data.profile.updatePhone !== undefined) {
- if (data.profile.updatePhone) {
- params.updatePhone = true;
- }
- data.profile = util.omit(data.profile, 'updatePhone');
- }
- var href = link.href + util.toQueryParams(params);
- return postToTransaction(sdk, href, data);
- };
- }
- }
- }
-
- function links2fns(sdk, res, obj, ref) {
- var fns = {};
- for (var linkName in obj._links) {
- if (!obj._links.hasOwnProperty(linkName)) {
- continue;
- }
-
- var link = obj._links[linkName];
-
- if (linkName === 'next') {
- linkName = link.name;
- }
-
- if (link.type) {
- fns[linkName] = link;
- continue;
- }
-
- switch (linkName) {
- // poll is only found at the transaction
- // level, so we don't need to pass the link
- case 'poll':
- fns.poll = getPollFn(sdk, res, ref);
- break;
-
- default:
- var fn = link2fn(sdk, res, obj, link, ref);
- if (fn) {
- fns[linkName] = fn;
- }
- }
- }
- return fns;
- }
-
- function flattenEmbedded(sdk, res, obj, ref) {
- obj = obj || res;
- obj = util.clone(obj);
-
- if (Array.isArray(obj)) {
- var objArr = [];
- for (var o = 0, ol = obj.length; o < ol; o++) {
- objArr.push(flattenEmbedded(sdk, res, obj[o], ref));
- }
- return objArr;
- }
-
- var embedded = obj._embedded || {};
-
- for (var key in embedded) {
- if (!embedded.hasOwnProperty(key)) {
- continue;
- }
-
- // Flatten any nested _embedded objects
- if (util.isObject(embedded[key]) || Array.isArray(embedded[key])) {
- embedded[key] = flattenEmbedded(sdk, res, embedded[key], ref);
- }
- }
-
- // Convert any links on the embedded object
- var fns = links2fns(sdk, res, obj, ref);
- util.extend(embedded, fns);
-
- obj = util.omit(obj, '_embedded', '_links');
- util.extend(obj, embedded);
- return obj;
- }
-
- function AuthTransaction(sdk, res) {
- if (res) {
- this.data = res;
- util.extend(this, flattenEmbedded(sdk, res, res, {}));
- delete this.stateToken;
-
- // RECOVERY_CHALLENGE has some responses without _links.
- // Without _links, we emulate cancel to make it intuitive
- // to return to the starting state. We may remove this
- // when OKTA-75434 is resolved
- if (res.status === 'RECOVERY_CHALLENGE' && !res._links) {
- this.cancel = function() {
- return new Q(new AuthTransaction(sdk));
- };
- }
- }
- }
-
- module.exports = {
- transactionStatus: transactionStatus,
- resumeTransaction: resumeTransaction,
- transactionExists: transactionExists,
- postToTransaction: postToTransaction
- };
|