From aea188abe5aba24106143a379724686eafd7b831 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Wed, 22 Jul 2026 16:25:41 +0530 Subject: [PATCH 1/3] fix: warn and skip deprecated guardian phone provider on 403 during import --- .../auth0/handlers/guardianFactorTemplates.ts | 24 +++++++---- .../guardianPhoneFactorMessageTypes.ts | 14 +++++-- .../guardianPhoneFactorSelectedProvider.ts | 14 +++++-- src/tools/utils.ts | 7 +++- .../handlers/guardianFactorTemplates.tests.js | 33 +++++++++++++++ test/tools/utils.test.js | 41 ++++++++++++++++++- 6 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/tools/auth0/handlers/guardianFactorTemplates.ts b/src/tools/auth0/handlers/guardianFactorTemplates.ts index aed973a10..c1d9b972f 100644 --- a/src/tools/auth0/handlers/guardianFactorTemplates.ts +++ b/src/tools/auth0/handlers/guardianFactorTemplates.ts @@ -67,14 +67,22 @@ export default class GuardianFactorTemplatesHandler extends DefaultHandler { guardianFactorTemplates.map(async (fatorTemplates) => { const { name, ...data } = fatorTemplates; const params = { name: fatorTemplates.name }; - if (name === 'sms') { - await this.client.guardian.factors.sms.setTemplates( - data as Management.SetGuardianFactorSmsTemplatesRequestContent - ); - } else if (name === 'phone') { - await this.client.guardian.factors.phone.setTemplates( - data as Management.SetGuardianFactorPhoneTemplatesRequestContent - ); + try { + if (name === 'sms') { + await this.client.guardian.factors.sms.setTemplates( + data as Management.SetGuardianFactorSmsTemplatesRequestContent + ); + } else if (name === 'phone') { + await this.client.guardian.factors.phone.setTemplates( + data as Management.SetGuardianFactorPhoneTemplatesRequestContent + ); + } + } catch (err) { + if (isForbiddenFeatureError(err, this.type)) { + // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import. + return; + } + throw err; } this.didUpdate(params); this.updated += 1; diff --git a/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts b/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts index ab32f5ef0..8b761de47 100644 --- a/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts +++ b/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts @@ -84,9 +84,17 @@ export default class GuardianPhoneMessageTypesHandler extends DefaultHandler { } const data = guardianPhoneFactorMessageTypes; - await this.client.guardian.factors.phone.setMessageTypes( - data as unknown as Management.SetGuardianFactorPhoneMessageTypesRequestContent - ); + try { + await this.client.guardian.factors.phone.setMessageTypes( + data as unknown as Management.SetGuardianFactorPhoneMessageTypesRequestContent + ); + } catch (err) { + if (isFeatureUnavailableError(err) || isForbiddenFeatureError(err, this.type)) { + // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import. + return; + } + throw err; + } this.updated += 1; this.didUpdate(guardianPhoneFactorMessageTypes); } diff --git a/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts b/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts index c40fffcd4..da7772aa2 100644 --- a/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts +++ b/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts @@ -81,9 +81,17 @@ export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler } const data = guardianPhoneFactorSelectedProvider; - await this.client.guardian.factors.phone.setProvider( - data as Management.SetGuardianFactorsProviderPhoneRequestContent - ); + try { + await this.client.guardian.factors.phone.setProvider( + data as Management.SetGuardianFactorsProviderPhoneRequestContent + ); + } catch (err) { + if (isFeatureUnavailableError(err) || isForbiddenFeatureError(err, this.type)) { + // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import. + return; + } + throw err; + } this.updated += 1; this.didUpdate(guardianPhoneFactorSelectedProvider); } diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 9e2e04702..4704be452 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -357,7 +357,12 @@ export const isDeprecatedError = (err: { message: string; statusCode: number }): export const isForbiddenFeatureError = (err, type): boolean => { if (err.statusCode === 403) { - log.warn(`${err.message};${err.errorCode ?? ''} - Skipping ${type}`); + // The SDK error's top-level `message` is the full serialized response body; the clean, + // human-readable message and errorCode live on the response body itself. + const body = err.originalError?.response?.body ?? {}; + const message = body.message ?? err.message; + const errorCode = body.errorCode ?? err.errorCode ?? ''; + log.warn(`${message}${errorCode ? ` (${errorCode})` : ''} - Skipping ${type}`); return true; } return false; diff --git a/test/tools/auth0/handlers/guardianFactorTemplates.tests.js b/test/tools/auth0/handlers/guardianFactorTemplates.tests.js index 2134390bb..ba772b120 100644 --- a/test/tools/auth0/handlers/guardianFactorTemplates.tests.js +++ b/test/tools/auth0/handlers/guardianFactorTemplates.tests.js @@ -122,5 +122,38 @@ describe('#guardianFactorTemplates handler', () => { await stageFn.apply(handler, [{ guardianFactorTemplates: data }]); }); + + it('should warn and skip when the legacy feature is not allowed on the tenant', async () => { + const auth0 = { + guardian: { + factors: { + sms: { + setTemplates: () => { + const err = new Error( + 'Insufficient privileges to use this deprecated feature. To handle Phone Provider/Templates refer to Tenant Phone Settings' + ); + err.statusCode = 403; + err.errorCode = 'legacy_mfa_phone_provider_not_allowed'; + return Promise.reject(err); + }, + }, + }, + }, + pool, + }; + + const handler = new guardianFactorTemplatesTests.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const data = [ + { + name: 'sms', + enrollment_message: 'test', + }, + ]; + + // Should resolve (not throw) even though the API rejects with a 403. + await stageFn.apply(handler, [{ guardianFactorTemplates: data }]); + expect(handler.updated).to.equal(0); + }); }); }); diff --git a/test/tools/utils.test.js b/test/tools/utils.test.js index 2111522a9..7bd3ef217 100644 --- a/test/tools/utils.test.js +++ b/test/tools/utils.test.js @@ -805,7 +805,7 @@ describe('#isForbiddenFeatureError', () => { // eslint-disable-next-line no-unused-expressions expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true; - expect(warnMessage).to.equal('Forbidden resource access; - Skipping connections'); + expect(warnMessage).to.equal('Forbidden resource access - Skipping connections'); // Restore original warn function log.warn = originalWarn; @@ -829,7 +829,44 @@ describe('#isForbiddenFeatureError', () => { // eslint-disable-next-line no-unused-expressions expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true; expect(warnMessage).to.equal( - 'Forbidden resource access;forbidden_resource_error - Skipping actions' + 'Forbidden resource access (forbidden_resource_error) - Skipping actions' + ); + + // Restore original warn function + log.warn = originalWarn; + }); + + it('should use the clean message and errorCode from the response body when available', () => { + let warnMessage; + const originalWarn = log.warn; + // Mock the log.warn function + log.warn = (msg) => { + warnMessage = msg; + }; + + // Mirrors the real SDK ForbiddenError: top-level `message` is the full serialized body, + // while the human-readable message and errorCode live on originalError.response.body. + const error = { + message: + 'ForbiddenError\nStatus code: 403\nBody: {\n "statusCode": 403,\n "errorCode": "legacy_mfa_phone_provider_not_allowed"\n}', + statusCode: 403, + originalError: { + response: { + body: { + statusCode: 403, + error: 'Forbidden', + message: 'Insufficient privileges to use this deprecated feature.', + errorCode: 'legacy_mfa_phone_provider_not_allowed', + }, + }, + }, + }; + const resourceType = 'guardianPhoneFactorSelectedProvider'; + + // eslint-disable-next-line no-unused-expressions + expect(utils.isForbiddenFeatureError(error, resourceType)).to.be.true; + expect(warnMessage).to.equal( + 'Insufficient privileges to use this deprecated feature. (legacy_mfa_phone_provider_not_allowed) - Skipping guardianPhoneFactorSelectedProvider' ); // Restore original warn function From 856089605bbd8d2ec672bc9ec1835930532bcbe7 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 23 Jul 2026 14:36:25 +0530 Subject: [PATCH 2/3] refactor: let isForbiddenFeatureError own 403s in guardian phone handlers --- .../auth0/handlers/guardianPhoneFactorMessageTypes.ts | 11 +---------- .../handlers/guardianPhoneFactorSelectedProvider.ts | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts b/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts index 8b761de47..e8ccf7cdd 100644 --- a/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts +++ b/src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts @@ -23,16 +23,7 @@ const isFeatureUnavailableError = (err): boolean => { // Older Management API version where the endpoint is not available. return true; } - if ( - err.statusCode === 403 && - err.originalError && - err.originalError.response && - err.originalError.response.body && - err.originalError.response.body.errorCode === 'voice_mfa_not_allowed' - ) { - // Recent Management API version, but with feature explicitly disabled. - return true; - } + // 403s (feature explicitly disabled) are handled by isForbiddenFeatureError. return false; }; diff --git a/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts b/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts index da7772aa2..84ebc3dba 100644 --- a/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts +++ b/src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts @@ -20,16 +20,7 @@ const isFeatureUnavailableError = (err) => { // Older Management API version where the endpoint is not available. return true; } - if ( - err.statusCode === 403 && - err.originalError && - err.originalError.response && - err.originalError.response.body && - err.originalError.response.body.errorCode === 'hooks_not_allowed' - ) { - // Recent Management API version, but with feature explicitly disabled. - return true; - } + // 403s (feature explicitly disabled) are handled by isForbiddenFeatureError. return false; }; From 54d8e2e17e831eab4ec1dc4f10beea83618d6ce9 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Tue, 28 Jul 2026 02:16:06 +0530 Subject: [PATCH 3/3] test: add 403 warn-and-skip tests for guardian phone message types and provider --- .../guardianPhoneFactorMessageTypes.tests.js | 26 +++++++++++++++++++ ...ardianPhoneFactorSelectedProvider.tests.js | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/test/tools/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js b/test/tools/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js index b6272db73..325e3178d 100644 --- a/test/tools/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js +++ b/test/tools/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js @@ -174,5 +174,31 @@ describe('#guardianPhoneFactorMessageTypes handler', () => { await stageFn.apply(handler, [{ guardianPhoneFactorMessageTypes: null }]); }); + + it('should warn and skip when the legacy feature is not allowed on the tenant', async () => { + const auth0 = { + guardian: { + factors: { + phone: { + setMessageTypes: () => { + const err = new Error('This endpoint is disabled for your tenant.'); + err.statusCode = 403; + err.errorCode = 'voice_mfa_not_allowed'; + return Promise.reject(err); + }, + }, + }, + }, + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + // Should resolve (not throw) even though the API rejects with a 403. + await stageFn.apply(handler, [ + { guardianPhoneFactorMessageTypes: { message_types: ['sms', 'voice'] } }, + ]); + expect(handler.updated).to.equal(0); + }); }); }); diff --git a/test/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js b/test/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js index 8a8702313..e1d53069d 100644 --- a/test/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js +++ b/test/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js @@ -174,5 +174,31 @@ describe('#guardianPhoneFactorSelectedProvider handler', () => { await stageFn.apply(handler, [{ guardianPhoneFactorSelectedProvider: null }]); }); + + it('should warn and skip when the legacy feature is not allowed on the tenant', async () => { + const auth0 = { + guardian: { + factors: { + phone: { + setProvider: () => { + const err = new Error('This endpoint is disabled for your tenant.'); + err.statusCode = 403; + err.errorCode = 'legacy_mfa_phone_provider_not_allowed'; + return Promise.reject(err); + }, + }, + }, + }, + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + // Should resolve (not throw) even though the API rejects with a 403. + await stageFn.apply(handler, [ + { guardianPhoneFactorSelectedProvider: { provider: 'twilio' } }, + ]); + expect(handler.updated).to.equal(0); + }); }); });