From f872b346a13badfad795cab104a2716f17938977 Mon Sep 17 00:00:00 2001 From: alexeystadnikov-okta Date: Thu, 26 Apr 2018 19:52:21 +0300 Subject: [PATCH] :seedling: Add support to use rememberDevice param in polling and verify. (#111) Similar to autoPush, we now support rememberDevice function that will be evaluated before making the request. Resolves: OKTA-158993 --- lib/tx.js | 28 ++- test/spec/mfa-challenge.js | 344 ++++++++++++++++++++++++++++++++++++- test/spec/mfa-required.js | 94 +++++++++- 3 files changed, 451 insertions(+), 15 deletions(-) diff --git a/lib/tx.js b/lib/tx.js index 40e162e79..d0d2f36a6 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -89,9 +89,18 @@ function getPollFn(sdk, res, ref) { else if (autoPush !== undefined && autoPush !== null) { opts.autoPush = !!autoPush; } - if (rememberDevice) { - opts.rememberDevice = true; + 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 @@ -210,9 +219,18 @@ function link2fn(sdk, res, obj, link, ref) { data = util.omit(data, 'autoPush'); } - if (data.rememberDevice !== undefined) { - if (data.rememberDevice) { - params.rememberDevice = true; + 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'); diff --git a/test/spec/mfa-challenge.js b/test/spec/mfa-challenge.js index a6f752f44..b035c09b9 100644 --- a/test/spec/mfa-challenge.js +++ b/test/spec/mfa-challenge.js @@ -50,7 +50,7 @@ define(function(require) { setup: { status: 'mfa-challenge-sms', request: { - uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify', + uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?rememberDevice=false', data: { stateToken: '00rt1IY9c6Q3RVc4a2jJPbS2uAtFNWJz_d8A26KTdF', passCode: '123456' @@ -238,6 +238,94 @@ define(function(require) { } }); + util.itMakesCorrectRequestResponse({ + title: 'allows verification with rememberDevice as a function', + setup: { + status: 'mfa-challenge-sms', + request: { + uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?rememberDevice=true', + data: { + stateToken: '00rt1IY9c6Q3RVc4a2jJPbS2uAtFNWJz_d8A26KTdF', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + return test.trans.verify({ + passCode: '123456', + rememberDevice: function () { + return true; + } + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'allows verification when rememberDevice function returns truthy value', + setup: { + status: 'mfa-challenge-sms', + request: { + uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?rememberDevice=true', + data: { + stateToken: '00rt1IY9c6Q3RVc4a2jJPbS2uAtFNWJz_d8A26KTdF', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + return test.trans.verify({ + passCode: '123456', + rememberDevice: function () { + return 'test'; + } + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'allows verification when rememberDevice function returns falsy value', + setup: { + status: 'mfa-challenge-sms', + request: { + uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?rememberDevice=false', + data: { + stateToken: '00rt1IY9c6Q3RVc4a2jJPbS2uAtFNWJz_d8A26KTdF', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + return test.trans.verify({ + passCode: '123456', + rememberDevice: function () { + return ''; + } + }); + } + }); + + util.itErrorsCorrectly({ + title: 'throws an error when rememberDevice function throws an error', + setup: { + status: 'mfa-challenge-sms' + }, + execute: function (test) { + return test.trans.verify({ + passCode: '123456', + rememberDevice: function () { + throw new Error('test'); + } + }); + }, + expectations: function (test, err) { + expect(err.name).toEqual('AuthSdkError'); + expect(err.errorSummary).toEqual('RememberDevice resulted in an error.'); + } + }); + util.itMakesCorrectRequestResponse({ title: 'allows verification with autoPush and rememberDevice true', setup: { @@ -265,7 +353,7 @@ define(function(require) { setup: { status: 'mfa-challenge-sms', request: { - uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?autoPush=false', + uri: '/api/v1/authn/factors/smsigwDlH85L9FyQK0g3/verify?autoPush=false&rememberDevice=false', data: { stateToken: '00rt1IY9c6Q3RVc4a2jJPbS2uAtFNWJz_d8A26KTdF', passCode: '123456' @@ -368,7 +456,7 @@ define(function(require) { }); util.itMakesCorrectRequestResponse({ - title: 'allows polling for push with rememberDevice', + title: 'allows polling for push with rememberDevice true', setup: { status: 'mfa-challenge-push', calls: [ @@ -409,6 +497,48 @@ define(function(require) { } }); + util.itMakesCorrectRequestResponse({ + title: 'allows polling for push with rememberDevice false', + setup: { + status: 'mfa-challenge-push', + calls: [ + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + return test.trans.poll({ + delay: 0, + rememberDevice: false + }); + } + }); + util.itMakesCorrectRequestResponse({ title: 'allows polling for push with autoPush true', setup: { @@ -693,6 +823,206 @@ define(function(require) { } }); + util.itMakesCorrectRequestResponse({ + title: 'allows polling for push with rememberDevice as a function', + setup: { + status: 'mfa-challenge-push', + calls: [ + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + return test.trans.poll({ + delay: 0, + rememberDevice: function () { + return true; + } + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'allows polling for push with rememberDevice value changing during poll', + setup: { + status: 'mfa-challenge-push', + calls: [ + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + var count = 1; + return test.trans.poll({ + delay: 0, + rememberDevice: function () { + if(count === 3) { + return false; + } + count ++; + return true; + } + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'allows polling for push when rememberDevice function returns truthy value', + setup: { + status: 'mfa-challenge-push', + calls: [ + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=true', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + return test.trans.poll({ + delay: 0, + rememberDevice: function () { + return 'test'; + } + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'allows polling for push when rememberDevice function returns falsy value', + setup: { + status: 'mfa-challenge-push', + calls: [ + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'mfa-challenge-push' + }, + { + request: { + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?rememberDevice=false', + data: { + stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + return test.trans.poll({ + delay: 0, + rememberDevice: function () { + return ''; + } + }); + } + }); + + util.itErrorsCorrectly({ + title: 'throws an error when rememberDevice function throws an error during polling', + setup: { + status: 'mfa-challenge-push' + }, + execute: function (test) { + return test.trans.poll({ + delay: 0, + rememberDevice: function () { + throw new Error('test'); + } + }); + }, + expectations: function (test, err) { + expect(err.name).toEqual('AuthSdkError'); + expect(err.errorSummary).toEqual('RememberDevice resulted in an error.'); + } + }); + util.itMakesCorrectRequestResponse({ title: 'does not include autoPush for polling for push if autoPush undefined', setup: { @@ -821,13 +1151,13 @@ define(function(require) { }); util.itMakesCorrectRequestResponse({ - title: 'does not include rememberDevice for polling for push if rememberDevice is falsy', + title: 'allows polling for push if rememberDevice is falsy', setup: { status: 'mfa-challenge-push', calls: [ { request: { - uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true', + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true&rememberDevice=false', data: { stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' } @@ -836,7 +1166,7 @@ define(function(require) { }, { request: { - uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true', + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true&rememberDevice=false', data: { stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' } @@ -845,7 +1175,7 @@ define(function(require) { }, { request: { - uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true', + uri: '/api/v1/authn/factors/opf492vmb3s1blLTs0h7/verify?autoPush=true&rememberDevice=false', data: { stateToken: '00T4jcVNRzJy5dkWJ4P7c9051dY3FUYY9O2zvbU_vI' } diff --git a/test/spec/mfa-required.js b/test/spec/mfa-required.js index e0d333f4c..7da9b2f89 100644 --- a/test/spec/mfa-required.js +++ b/test/spec/mfa-required.js @@ -87,11 +87,11 @@ define(function(require) { } }); util.itMakesCorrectRequestResponse({ - title: 'doesn\'t pass rememberDevice as a query param if falsy', + title: 'passes rememberDevice as a query param if falsy', setup: { status: 'mfa-required', request: { - uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify', + uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?rememberDevice=false', data: { stateToken: '004KscPNUS2LswGp26qiu4Hetqt_zcgz-PcQhPseVP', passCode: '123456' @@ -306,7 +306,7 @@ define(function(require) { setup: { status: 'mfa-required', request: { - uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?autoPush=true', + uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?autoPush=true&rememberDevice=false', data: { stateToken: '004KscPNUS2LswGp26qiu4Hetqt_zcgz-PcQhPseVP', passCode: '123456' @@ -323,6 +323,94 @@ define(function(require) { }); } }); + util.itMakesCorrectRequestResponse({ + title: 'passes rememberDevice as a query param if function returns true', + setup: { + status: 'mfa-required', + request: { + uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?rememberDevice=true', + data: { + stateToken: '004KscPNUS2LswGp26qiu4Hetqt_zcgz-PcQhPseVP', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + var factor = _.find(test.trans.factors, {id: 'uftigiEmYTPOmvqTS0g3'}); + return factor.verify({ + passCode: '123456', + rememberDevice: function() { + return true; + } + }); + } + }); + util.itMakesCorrectRequestResponse({ + title: 'passes rememberDevice as a boolean query param if function returns a truthy value', + setup: { + status: 'mfa-required', + request: { + uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?rememberDevice=true', + data: { + stateToken: '004KscPNUS2LswGp26qiu4Hetqt_zcgz-PcQhPseVP', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + var factor = _.find(test.trans.factors, {id: 'uftigiEmYTPOmvqTS0g3'}); + return factor.verify({ + passCode: '123456', + rememberDevice: function() { + return 'test'; + } + }); + } + }); + util.itMakesCorrectRequestResponse({ + title: 'passes rememberDevice as a boolean query param if function returns a falsy value', + setup: { + status: 'mfa-required', + request: { + uri: '/api/v1/authn/factors/uftigiEmYTPOmvqTS0g3/verify?rememberDevice=false', + data: { + stateToken: '004KscPNUS2LswGp26qiu4Hetqt_zcgz-PcQhPseVP', + passCode: '123456' + } + }, + response: 'success' + }, + execute: function (test) { + var factor = _.find(test.trans.factors, {id: 'uftigiEmYTPOmvqTS0g3'}); + return factor.verify({ + passCode: '123456', + rememberDevice: function() { + return ''; + } + }); + } + }); + util.itErrorsCorrectly({ + title: 'throws an error when rememberDevice function throws an error', + setup: { + status: 'mfa-required' + }, + execute: function (test) { + var factor = _.find(test.trans.factors, {id: 'uftigiEmYTPOmvqTS0g3'}); + return factor.verify({ + passCode: '123456', + rememberDevice: function () { + throw new Error('test'); + } + }); + }, + expectations: function (test, err) { + expect(err.name).toEqual('AuthSdkError'); + expect(err.errorSummary).toEqual('RememberDevice resulted in an error.'); + } + }); util.itMakesCorrectRequestResponse({ title: 'passes rememberDevice as a query param if autoPush is undefined and rememberDevice is true', setup: {