From 7973024290610fe3b7144ba8ac1c7604a4ccbbc1 Mon Sep 17 00:00:00 2001 From: Christopher Rogers Date: Tue, 24 Sep 2024 17:12:09 -0700 Subject: [PATCH 1/2] Simplifies proactive 3-D Secure APIs --- lib/recurly.js | 1 + lib/recurly/risk/risk.js | 13 +++-- .../risk/three-d-secure/strategy/braintree.js | 28 +++++----- .../risk/three-d-secure/three-d-secure.js | 52 +++++++------------ lib/recurly/token.js | 19 +------ types/lib/configure.d.ts | 1 + 6 files changed, 42 insertions(+), 72 deletions(-) diff --git a/lib/recurly.js b/lib/recurly.js index 61e73b17..19181a9c 100644 --- a/lib/recurly.js +++ b/lib/recurly.js @@ -74,6 +74,7 @@ const DEFAULTS = { preflightDeviceDataCollector: true, proactive: { enabled: false, + gatewayCode: '' } } }, diff --git a/lib/recurly/risk/risk.js b/lib/recurly/risk/risk.js index 31dea191..49fcdd83 100644 --- a/lib/recurly/risk/risk.js +++ b/lib/recurly/risk/risk.js @@ -56,15 +56,14 @@ export class Risk { * @return {Promise} */ static preflight ({ recurly, number, month, year, cvv }) { - function resolveRoute (recurly) { - let route = '/risk/preflights'; - if (recurly.config.risk.threeDSecure.proactive.enabled) { - route += `?proactive=true&gatewayCode=${recurly.config.risk.threeDSecure.proactive.gateway_code}`; - } - return route; + const data = {}; + + if (recurly.config.risk.threeDSecure.proactive.enabled) { + data.proactive = true; + data.gateway_code = recurly.config.risk.threeDSecure.proactive.gatewayCode; } - return recurly.request.get({ route: resolveRoute(recurly) }) + return recurly.request.get({ route: '/risk/preflights', data }) .then(({ preflights }) => { debug('received preflight instructions', preflights); return ThreeDSecure.preflight({ recurly, number, month, year, cvv, preflights }); diff --git a/lib/recurly/risk/three-d-secure/strategy/braintree.js b/lib/recurly/risk/three-d-secure/strategy/braintree.js index ef59113f..175b2148 100644 --- a/lib/recurly/risk/three-d-secure/strategy/braintree.js +++ b/lib/recurly/risk/three-d-secure/strategy/braintree.js @@ -11,31 +11,31 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy { } static preflight ({ recurly, number, month, year, cvv }) { - const { proactive } = recurly.config.risk.threeDSecure; + const { enabled, gatewayCode } = recurly.config.risk.threeDSecure.proactive; - if(!proactive.enabled) { + debug('performing preflight for', { gatewayCode }); + + if (!enabled) { return Promise.resolve(); } const data = { - gatewayType: BraintreeStrategy.strategyName, - gatewayCode: proactive.gateway_code, + gateway_type: BraintreeStrategy.strategyName, + gateway_code: gatewayCode, number, month, year, - cvv, + cvv }; - // we don't really need to do anything once we get a response except resolve with relevant data instead of session_id + // we don't really need to do anything once we get a response except + // resolve with relevant data instead of session_id return recurly.request.post({ route: '/risk/authentications', data }) - .then(({ paymentMethodNonce, clientToken, bin }) => ( - { - payment_method_nonce: paymentMethodNonce, - client_token: clientToken, - bin, - proactive: true - } - )); + .then(({ paymentMethodNonce, clientToken, bin }) => ({ + payment_method_nonce: paymentMethodNonce, + client_token: clientToken, + bin, + })); } diff --git a/lib/recurly/risk/three-d-secure/three-d-secure.js b/lib/recurly/risk/three-d-secure/three-d-secure.js index d9c4c773..825d0224 100644 --- a/lib/recurly/risk/three-d-secure/three-d-secure.js +++ b/lib/recurly/risk/three-d-secure/three-d-secure.js @@ -71,6 +71,11 @@ export class ThreeDSecure extends RiskConcern { '05': { height: '100%', width: '100%' } } + static VALID_ACTION_TOKEN_TYPES = [ + 'three_d_secure_action', + 'three_d_secure_proactive_action' + ]; + /** * Returns a strateggy for a given gateway type * @@ -108,7 +113,7 @@ export class ThreeDSecure extends RiskConcern { }, Promise.resolve([])); } - constructor ({ risk, actionTokenId, challengeWindowSize, proactiveTokenId }) { + constructor ({ risk, actionTokenId, challengeWindowSize }) { const existingConcern = risk.concerns.find((concern) => concern instanceof ThreeDSecure); if (existingConcern) { throw errors('3ds-multiple-instances', { name: 'ThreeDSecure', expect: 'to be the only concern' }); @@ -117,28 +122,25 @@ export class ThreeDSecure extends RiskConcern { super({ risk }); this.actionTokenId = actionTokenId; - this.proactiveTokenId = proactiveTokenId; this.validateChallengeWindowSize(challengeWindowSize); this.challengeWindowSize = challengeWindowSize || this.constructor.CHALLENGE_WINDOW_SIZE_DEFAULT; - if (!actionTokenId && !proactiveTokenId) { + if (!actionTokenId) { throw errors('invalid-option', { name: 'actionTokenId', expect: 'a three_d_secure_action_token_id' }); } - this.recurly.request.get({ route: `/tokens/${this.resolveToken()}` }) + this.recurly.request.get({ route: `/tokens/${actionTokenId}` }) .catch(err => this.error(err)) .then(token => { - if (this.resolveToken() == this.actionTokenId) { - this.resolveActionToken(token); - } else { - this.resolveProactiveToken(token); - } + assertIsActionToken(token); + this.strategy = this.getStrategyForActionToken(token); + this.strategy.on('done', (...args) => this.onStrategyDone(...args)); this.markReady(); }) .catch(err => this.error(err)); - this.report('create', { actionTokenId, proactiveTokenId }); + this.report('create', { actionTokenId }); this.whenReady(() => this.report('ready', { strategy: this.strategy.strategyName })); } @@ -172,11 +174,6 @@ export class ThreeDSecure extends RiskConcern { return new strategy({ threeDSecure: this, actionToken }); } - getStrategyForProactiveToken (token) { - const strategy = ThreeDSecure.getStrategyForGatewayType(token.three_d_secure.gateway.type); - return new strategy({ threeDSecure: this, proactiveToken: token }); - } - /** * Creates a ThreeDSecureActionResultToken from action results * @@ -189,9 +186,9 @@ export class ThreeDSecure extends RiskConcern { const data = { type: 'three_d_secure_action_result', three_d_secure_action_token_id: this.actionTokenId, - proactive_three_d_secure_token_id: this.proactiveTokenId, results }; + debug('submitting results for tokenization', data); return this.recurly.request.post({ route: '/tokens', data }); } @@ -225,24 +222,13 @@ export class ThreeDSecure extends RiskConcern { throw new Error(`Invalid challengeWindowSize. Expected any of ${validWindowSizes}, got ${challengeWindowSize}`); } } - - resolveToken () { - return this.actionTokenId || this.proactiveTokenId; - } - - resolveActionToken (token) { - assertIsActionToken(token); - this.strategy = this.getStrategyForActionToken(token); - this.strategy.on('done', (...args) => this.onStrategyDone(...args)); - } - - resolveProactiveToken (token) { - this.strategy = this.getStrategyForProactiveToken(token); - this.strategy.on('done', (...args) => this.onStrategyDone(...args)); - } } function assertIsActionToken (token) { - if (token && token.type === 'three_d_secure_action') return; - throw errors('invalid-option', { name: 'actionTokenId', expect: 'a three_d_secure_action_token_id' }); + if (VALID_ACTION_TOKEN_TYPES.includes(token?.type)) return; + + throw errors('invalid-option', { + name: 'actionTokenId', + expect: `a token of type: ${VALID_ACTION_TOKEN_TYPES.join(',')}` + }); } diff --git a/lib/recurly/token.js b/lib/recurly/token.js index 0ba58247..393c3e4a 100644 --- a/lib/recurly/token.js +++ b/lib/recurly/token.js @@ -174,9 +174,7 @@ function token (customerData, bus, done) { const { number, month, year, cvv } = inputs; Risk.preflight({ recurly: this, number, month, year, cvv }) - .then(results => { - enrichInputs(this, inputs, results); - }) + .then(results => inputs.risk = results) .then(() => this.request.post({ route: '/token', data: inputs, done: complete })) .done(); } @@ -188,19 +186,4 @@ function token (customerData, bus, done) { } done(null, res); } - - function enrichInputs (recurly, inputs, results) { - if (results.length === 0) return; - - inputs.risk = []; - - results.forEach(result => { - if (result.processor === 'braintree_blue') { - inputs.proactive = recurly.config.risk.threeDSecure.proactive; - inputs.proactive.params = result; - } else { - inputs.risk.push(result); - } - }); - } } diff --git a/types/lib/configure.d.ts b/types/lib/configure.d.ts index 050ab7c0..70d000e4 100644 --- a/types/lib/configure.d.ts +++ b/types/lib/configure.d.ts @@ -26,6 +26,7 @@ export type RecurlyOptions = { preflightDeviceDataCollector?: boolean; proactive?: { enabled: true; + gatewayCode: string; } } }; From 9a728f343db4e25bbf59b28ebe5a41d2ac723e61 Mon Sep 17 00:00:00 2001 From: gil Date: Fri, 27 Sep 2024 10:24:27 -0700 Subject: [PATCH 2/2] fix bugs --- lib/recurly/risk/risk.js | 15 ++++++---- .../risk/three-d-secure/strategy/braintree.js | 12 +++++--- .../three-d-secure/strategy/cybersource.js | 2 +- .../risk/three-d-secure/strategy/worldpay.js | 2 +- .../risk/three-d-secure/three-d-secure.js | 28 ++++++++++++++----- lib/recurly/token.js | 5 +++- 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/lib/recurly/risk/risk.js b/lib/recurly/risk/risk.js index 49fcdd83..91c744e2 100644 --- a/lib/recurly/risk/risk.js +++ b/lib/recurly/risk/risk.js @@ -68,12 +68,15 @@ export class Risk { debug('received preflight instructions', preflights); return ThreeDSecure.preflight({ recurly, number, month, year, cvv, preflights }); }) - .then(results => results.filter(maybeErr => { - if (maybeErr.code === 'risk-preflight-timeout') { - debug('timeout encountered', maybeErr); - return false; - } - return true; + .then(({ tokenType, risk }) => ({ + risk: risk.filter(maybeErr => { + if (maybeErr.code === 'risk-preflight-timeout') { + debug('timeout encountered', maybeErr); + return false; + } + return true; + }), + tokenType })); } diff --git a/lib/recurly/risk/three-d-secure/strategy/braintree.js b/lib/recurly/risk/three-d-secure/strategy/braintree.js index 175b2148..782963fd 100644 --- a/lib/recurly/risk/three-d-secure/strategy/braintree.js +++ b/lib/recurly/risk/three-d-secure/strategy/braintree.js @@ -11,7 +11,7 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy { } static preflight ({ recurly, number, month, year, cvv }) { - const { enabled, gatewayCode } = recurly.config.risk.threeDSecure.proactive; + const { enabled, gatewayCode, amount } = recurly.config.risk.threeDSecure.proactive; debug('performing preflight for', { gatewayCode }); @@ -32,9 +32,13 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy { // resolve with relevant data instead of session_id return recurly.request.post({ route: '/risk/authentications', data }) .then(({ paymentMethodNonce, clientToken, bin }) => ({ - payment_method_nonce: paymentMethodNonce, - client_token: clientToken, - bin, + results: { + payment_method_nonce: paymentMethodNonce, + client_token: clientToken, + bin, + amount: amount + }, + tokenType: 'three_d_secure_proactive_action' })); } diff --git a/lib/recurly/risk/three-d-secure/strategy/cybersource.js b/lib/recurly/risk/three-d-secure/strategy/cybersource.js index 3c22f163..eaad4839 100644 --- a/lib/recurly/risk/three-d-secure/strategy/cybersource.js +++ b/lib/recurly/risk/three-d-secure/strategy/cybersource.js @@ -44,7 +44,7 @@ export default class CybersourceStrategy extends ThreeDSecureStrategy { const body = JSON.parse(data); if (body.MessageType === 'profile.completed') { debug('received device data session id', body); - resolve({ session_id: body.SessionId }); + resolve({ results: { session_id: body.SessionId }}); frame.destroy(); recurly.bus.off('raw-message', listener); } diff --git a/lib/recurly/risk/three-d-secure/strategy/worldpay.js b/lib/recurly/risk/three-d-secure/strategy/worldpay.js index 38f95886..52d1fbc6 100644 --- a/lib/recurly/risk/three-d-secure/strategy/worldpay.js +++ b/lib/recurly/risk/three-d-secure/strategy/worldpay.js @@ -44,7 +44,7 @@ export default class WorldpayStrategy extends ThreeDSecureStrategy { const body = JSON.parse(data); if (body.MessageType === 'profile.completed') { debug('received device data session id', body); - resolve({ session_id: body.SessionId }); + resolve({ results: { session_id: body.SessionId }}); recurly.bus.off('raw-message', listener); frame.destroy(); } diff --git a/lib/recurly/risk/three-d-secure/three-d-secure.js b/lib/recurly/risk/three-d-secure/three-d-secure.js index 825d0224..ba3efd48 100644 --- a/lib/recurly/risk/three-d-secure/three-d-secure.js +++ b/lib/recurly/risk/three-d-secure/three-d-secure.js @@ -102,15 +102,29 @@ export class ThreeDSecure extends RiskConcern { static preflight ({ recurly, number, month, year, cvv, preflights }) { return preflights.reduce((preflight, result) => { return preflight.then((finishedPreflights) => { - const { type } = result.gateway; + const { type: gatewayType } = result.gateway; const { gateway_code } = result.params; - const strategy = ThreeDSecure.getStrategyForGatewayType(type); + const strategy = ThreeDSecure.getStrategyForGatewayType(gatewayType); return strategy.preflight({ recurly, number, month, year, cvv, ...result.params }) - .then(results => { - return finishedPreflights.concat([{ processor: type, gateway_code, results }]); + .then(({ results, tokenType }) => { + // return finishedPreflights.concat([{ processor: type, gateway_code, results}]); + return { + tokenType: finishedPreflights.tokenType || tokenType, + // risk: { + // processor: gatewayType, + // gateway_code, + // risk + // // finishedPreflights.risk.concat(risk) + // } + risk: finishedPreflights.risk.concat({ + processor: gatewayType, + gateway_code, + results + }) + } }); }); - }, Promise.resolve([])); + }, Promise.resolve({risk: []})); } constructor ({ risk, actionTokenId, challengeWindowSize }) { @@ -225,10 +239,10 @@ export class ThreeDSecure extends RiskConcern { } function assertIsActionToken (token) { - if (VALID_ACTION_TOKEN_TYPES.includes(token?.type)) return; + if (ThreeDSecure.VALID_ACTION_TOKEN_TYPES.includes(token?.type)) return; throw errors('invalid-option', { name: 'actionTokenId', - expect: `a token of type: ${VALID_ACTION_TOKEN_TYPES.join(',')}` + expect: `a token of type: ${ThreeDSecure.VALID_ACTION_TOKEN_TYPES.join(',')}` }); } diff --git a/lib/recurly/token.js b/lib/recurly/token.js index 393c3e4a..e6959c88 100644 --- a/lib/recurly/token.js +++ b/lib/recurly/token.js @@ -174,7 +174,10 @@ function token (customerData, bus, done) { const { number, month, year, cvv } = inputs; Risk.preflight({ recurly: this, number, month, year, cvv }) - .then(results => inputs.risk = results) + .then(({ risk, tokenType }) => { + inputs.risk = risk + if (tokenType) inputs.type = tokenType + }) .then(() => this.request.post({ route: '/token', data: inputs, done: complete })) .done(); }