From 72b3e988562e0747ae1f5d756bb8739cad64282a Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 10 Jul 2024 15:10:39 -0700 Subject: [PATCH 1/8] enable recaptcha enterprise on phone auth --- etc/firebase-admin.auth.api.md | 1 + src/auth/auth-config.ts | 55 ++++++++++++++++++++++++++-------- test/unit/auth/tenant.spec.ts | 46 ++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index d921458bec..2b581a0a77 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -877,6 +877,7 @@ export type RecaptchaAction = 'BLOCK'; export interface RecaptchaConfig { emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; managedRules?: RecaptchaManagedRule[]; + phoneEnforcementState?: RecaptchaProviderEnforcementState; recaptchaKeys?: RecaptchaKey[]; useAccountDefender?: boolean; } diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 28ee595c46..a53e3e51db 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -509,14 +509,14 @@ export interface MultiFactorConfig { factorIds?: AuthFactorType[]; /** - * A list of multi-factor provider configurations. + * A list of multi-factor provider configurations. * MFA providers (except phone) indicate whether they're enabled through this field. */ providerConfigs?: MultiFactorProviderConfig[]; } /** - * Interface representing a multi-factor auth provider configuration. - * This interface is used for second factor auth providers other than SMS. + * Interface representing a multi-factor auth provider configuration. + * This interface is used for second factor auth providers other than SMS. * Currently, only TOTP is supported. */export interface MultiFactorProviderConfig { /** @@ -528,7 +528,7 @@ export interface MultiFactorConfig { } /** - * Interface representing configuration settings for TOTP second factor auth. + * Interface representing configuration settings for TOTP second factor auth. */ export interface TotpMultiFactorProviderConfig { /** @@ -540,7 +540,7 @@ export interface TotpMultiFactorProviderConfig { /** * Defines the multi-factor config class used to convert client side MultiFactorConfig * to a format that is understood by the Auth server. - * + * * @internal */ export class MultiFactorAuthConfig implements MultiFactorConfig { @@ -555,7 +555,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { */ public readonly factorIds: AuthFactorType[]; /** - * A list of multi-factor provider specific config. + * A list of multi-factor provider specific config. * New MFA providers (except phone) will indicate enablement/disablement through this field. */ public readonly providerConfigs: MultiFactorProviderConfig[]; @@ -1781,6 +1781,10 @@ export interface RecaptchaConfig { * The enforcement state of the email password provider. */ emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + /** + * The enforcement state of the phone provider. + */ + phoneEnforcementState?: RecaptchaProviderEnforcementState; /** * The reCAPTCHA managed rules. */ @@ -1800,12 +1804,14 @@ export interface RecaptchaConfig { export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + public readonly phoneEnforcementState?: RecaptchaProviderEnforcementState; public readonly managedRules?: RecaptchaManagedRule[]; public readonly recaptchaKeys?: RecaptchaKey[]; public readonly useAccountDefender?: boolean; constructor(recaptchaConfig: RecaptchaConfig) { this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; + this.phoneEnforcementState = recaptchaConfig.phoneEnforcementState; this.managedRules = recaptchaConfig.managedRules; this.recaptchaKeys = recaptchaConfig.recaptchaKeys; this.useAccountDefender = recaptchaConfig.useAccountDefender; @@ -1818,6 +1824,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { public static validate(options: RecaptchaConfig): void { const validKeys = { emailPasswordEnforcementState: true, + phoneEnforcementState: true, managedRules: true, recaptchaKeys: true, useAccountDefender: true, @@ -1858,6 +1865,24 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } + if (typeof options.phoneEnforcementState !== undefined) { + if (!validator.isNonEmptyString(options.phoneEnforcementState)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.', + ); + } + + if (options.phoneEnforcementState !== 'OFF' && + options.phoneEnforcementState !== 'AUDIT' && + options.phoneEnforcementState !== 'ENFORCE') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', + ); + } + } + if (typeof options.managedRules !== 'undefined') { // Validate array if (!validator.isArray(options.managedRules)) { @@ -1924,6 +1949,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { public toJSON(): object { const json: any = { emailPasswordEnforcementState: this.emailPasswordEnforcementState, + phoneEnforcementState: this.phoneEnforcementState, managedRules: deepCopy(this.managedRules), recaptchaKeys: deepCopy(this.recaptchaKeys), useAccountDefender: this.useAccountDefender, @@ -1932,6 +1958,9 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof json.emailPasswordEnforcementState === 'undefined') { delete json.emailPasswordEnforcementState; } + if (typeof json.phoneEnforcementState === 'undefined') { + delete json.phoneEnforcementState; + } if (typeof json.managedRules === 'undefined') { delete json.managedRules; } @@ -1947,8 +1976,8 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } -/** - * A password policy configuration for a project or tenant +/** + * A password policy configuration for a project or tenant */ export interface PasswordPolicyConfig { /** @@ -2003,7 +2032,7 @@ export interface CustomStrengthOptionsConfig { /** * Defines the password policy config class used to convert client side PasswordPolicyConfig * to a format that is understood by the Auth server. - * + * * @internal */ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { @@ -2110,7 +2139,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { '"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".', ); } - + if (typeof options.forceUpgradeOnSignin !== 'undefined') { if (!validator.isBoolean(options.forceUpgradeOnSignin)) { throw new FirebaseAuthError( @@ -2252,7 +2281,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { } } -/** +/** * Server side password policy configuration. */ export interface PasswordPolicyAuthServerConfig { @@ -2262,14 +2291,14 @@ export interface PasswordPolicyAuthServerConfig { } /** - * Server side password policy versions configuration. + * Server side password policy versions configuration. */ export interface PasswordPolicyVersionsAuthServerConfig { customStrengthOptions?: CustomStrengthOptionsAuthServerConfig; } /** - * Server side password policy constraints configuration. + * Server side password policy constraints configuration. */ export interface CustomStrengthOptionsAuthServerConfig { containsLowercaseCharacter?: boolean; diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index e1006e47b7..9a8e6a9549 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -297,6 +297,23 @@ describe('Tenant', () => { }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); + it('should throw on null phoneEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.phoneEnforcementState = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid phoneEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .phoneEnforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + it('should throw on non-array managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; @@ -385,7 +402,7 @@ describe('Tenant', () => { it('should throw on non-array disallowedRegions attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); @@ -455,7 +472,7 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); }); - + it('should throw on invalid constraints attribute', ()=> { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; @@ -537,7 +554,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.'); }); @@ -688,6 +705,23 @@ describe('Tenant', () => { }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); + it('should throw on null phoneEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.phoneEnforcementState = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid phoneEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .phoneEnforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + it('should throw on non-array managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; @@ -780,7 +814,7 @@ describe('Tenant', () => { it('should throw on non-array disallowedRegions attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); @@ -850,7 +884,7 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); }); - + it('should throw on invalid constraints attribute', ()=> { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; @@ -932,7 +966,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.'); }); From a55186cac4403ff6b9b8a3f0c675ffe5791bbbd3 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 10 Jul 2024 15:26:43 -0700 Subject: [PATCH 2/8] undo autosave editor changes --- test/unit/auth/tenant.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 9a8e6a9549..5050ed1332 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -402,7 +402,7 @@ describe('Tenant', () => { it('should throw on non-array disallowedRegions attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); @@ -472,7 +472,7 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); }); - + it('should throw on invalid constraints attribute', ()=> { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; @@ -554,7 +554,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.'); }); @@ -814,7 +814,7 @@ describe('Tenant', () => { it('should throw on non-array disallowedRegions attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); @@ -884,7 +884,7 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); }); - + it('should throw on invalid constraints attribute', ()=> { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; @@ -966,7 +966,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.'); }); From efb6a1b4fe0782f7dde1f7c8da79a7df1f66e81d Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 10 Jul 2024 15:29:42 -0700 Subject: [PATCH 3/8] remove editor whitespace changes --- src/auth/auth-config.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index a53e3e51db..797b1fa8e5 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -509,14 +509,14 @@ export interface MultiFactorConfig { factorIds?: AuthFactorType[]; /** - * A list of multi-factor provider configurations. + * A list of multi-factor provider configurations. * MFA providers (except phone) indicate whether they're enabled through this field. */ providerConfigs?: MultiFactorProviderConfig[]; } /** - * Interface representing a multi-factor auth provider configuration. - * This interface is used for second factor auth providers other than SMS. + * Interface representing a multi-factor auth provider configuration. + * This interface is used for second factor auth providers other than SMS. * Currently, only TOTP is supported. */export interface MultiFactorProviderConfig { /** @@ -528,7 +528,7 @@ export interface MultiFactorConfig { } /** - * Interface representing configuration settings for TOTP second factor auth. + * Interface representing configuration settings for TOTP second factor auth. */ export interface TotpMultiFactorProviderConfig { /** @@ -540,7 +540,7 @@ export interface TotpMultiFactorProviderConfig { /** * Defines the multi-factor config class used to convert client side MultiFactorConfig * to a format that is understood by the Auth server. - * + * * @internal */ export class MultiFactorAuthConfig implements MultiFactorConfig { @@ -555,7 +555,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { */ public readonly factorIds: AuthFactorType[]; /** - * A list of multi-factor provider specific config. + * A list of multi-factor provider specific config. * New MFA providers (except phone) will indicate enablement/disablement through this field. */ public readonly providerConfigs: MultiFactorProviderConfig[]; @@ -1781,7 +1781,7 @@ export interface RecaptchaConfig { * The enforcement state of the email password provider. */ emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; - /** + /** * The enforcement state of the phone provider. */ phoneEnforcementState?: RecaptchaProviderEnforcementState; @@ -1976,8 +1976,8 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } -/** - * A password policy configuration for a project or tenant +/** + * A password policy configuration for a project or tenant */ export interface PasswordPolicyConfig { /** @@ -2032,7 +2032,7 @@ export interface CustomStrengthOptionsConfig { /** * Defines the password policy config class used to convert client side PasswordPolicyConfig * to a format that is understood by the Auth server. - * + * * @internal */ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { @@ -2139,7 +2139,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { '"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".', ); } - + if (typeof options.forceUpgradeOnSignin !== 'undefined') { if (!validator.isBoolean(options.forceUpgradeOnSignin)) { throw new FirebaseAuthError( @@ -2281,7 +2281,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { } } -/** +/** * Server side password policy configuration. */ export interface PasswordPolicyAuthServerConfig { @@ -2291,14 +2291,14 @@ export interface PasswordPolicyAuthServerConfig { } /** - * Server side password policy versions configuration. + * Server side password policy versions configuration. */ export interface PasswordPolicyVersionsAuthServerConfig { customStrengthOptions?: CustomStrengthOptionsAuthServerConfig; } /** - * Server side password policy constraints configuration. + * Server side password policy constraints configuration. */ export interface CustomStrengthOptionsAuthServerConfig { containsLowercaseCharacter?: boolean; From 92dbff10cde3c99aaaf5b66c8ef3ad11e7be6f3a Mon Sep 17 00:00:00 2001 From: Pragati Date: Mon, 29 Jul 2024 15:12:48 -0700 Subject: [PATCH 4/8] fix unit tests + add sms tf changes --- src/auth/auth-config.ts | 154 ++++++++++++- src/auth/index.ts | 1 + src/auth/project-config.ts | 15 +- test/integration/auth.spec.ts | 127 +++++----- test/unit/auth/project-config.spec.ts | 318 +++++++++++++++++++------- test/unit/auth/tenant.spec.ts | 178 +++++++++++--- 6 files changed, 594 insertions(+), 199 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 797b1fa8e5..8531ec9103 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1750,6 +1750,21 @@ export interface RecaptchaManagedRule { action?: RecaptchaAction; } +/** + * The managed rules for toll fraud provider, containing the enforcement status. + * The toll fraud provider contains all SMS related user flows. + */ +export interface RecaptchaTollFraudManagedRule { + /** + * The action will be enforced if the reCAPTCHA score of a request is larger than endScore. + */ + startScore: number; + /** + * The action for reCAPTCHA-protected requests. + */ + action?: RecaptchaAction; +} + /** * The key's platform type. */ @@ -1784,22 +1799,35 @@ export interface RecaptchaConfig { /** * The enforcement state of the phone provider. */ - phoneEnforcementState?: RecaptchaProviderEnforcementState; + phoneEnforcementState?: RecaptchaProviderEnforcementState; /** * The reCAPTCHA managed rules. */ managedRules?: RecaptchaManagedRule[]; - /** * The reCAPTCHA keys. */ recaptchaKeys?: RecaptchaKey[]; - /** * Whether to use account defender for reCAPTCHA assessment. * The default value is false. */ useAccountDefender?: boolean; + /** + * Whether to use the rCE bot score for reCAPTCHA phone provider. + * Can only be true when the phone_enforcement_state is AUDIT or ENFORCE. + */ + useSmsBotScore?: boolean; + /* + * Whether to use the rCE sms toll fraud protection risk score for reCAPTCHA phone provider. + * Can only be true when the phone_enforcement_state is AUDIT or ENFORCE. + */ + useSmsTollFraudProtection?: boolean; + /** + * The managed rules for toll fraud provider, containing the enforcement status. + * The toll fraud provider contains all SMS related user flows. + */ + tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; } export class RecaptchaAuthConfig implements RecaptchaConfig { @@ -1808,13 +1836,35 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly managedRules?: RecaptchaManagedRule[]; public readonly recaptchaKeys?: RecaptchaKey[]; public readonly useAccountDefender?: boolean; + public readonly useSmsBotScore?: boolean; + public readonly useSmsTollFraudProtection?: boolean; + public readonly tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; constructor(recaptchaConfig: RecaptchaConfig) { - this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; - this.phoneEnforcementState = recaptchaConfig.phoneEnforcementState; - this.managedRules = recaptchaConfig.managedRules; - this.recaptchaKeys = recaptchaConfig.recaptchaKeys; - this.useAccountDefender = recaptchaConfig.useAccountDefender; + if (typeof recaptchaConfig.emailPasswordEnforcementState !== 'undefined') { + this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; + } + if (typeof recaptchaConfig.phoneEnforcementState !== 'undefined') { + this.phoneEnforcementState = recaptchaConfig.phoneEnforcementState; + } + if (typeof recaptchaConfig.managedRules !== 'undefined') { + this.managedRules = recaptchaConfig.managedRules; + } + if (typeof recaptchaConfig.recaptchaKeys !== 'undefined') { + this.recaptchaKeys = recaptchaConfig.recaptchaKeys; + } + if (typeof recaptchaConfig.useAccountDefender !== 'undefined') { + this.useAccountDefender = recaptchaConfig.useAccountDefender; + } + if (typeof recaptchaConfig.useSmsBotScore !== 'undefined') { + this.useSmsBotScore = recaptchaConfig.useSmsBotScore; + } + if (typeof recaptchaConfig.useSmsTollFraudProtection !== 'undefined') { + this.useSmsTollFraudProtection = recaptchaConfig.useSmsTollFraudProtection; + } + if (typeof recaptchaConfig.tollFraudManagedRules !== 'undefined') { + this.tollFraudManagedRules = recaptchaConfig.tollFraudManagedRules; + } } /** @@ -1828,6 +1878,9 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { managedRules: true, recaptchaKeys: true, useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + tollFraudManagedRules: true, }; if (!validator.isNonNullObject(options)) { @@ -1847,7 +1900,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } // Validation - if (typeof options.emailPasswordEnforcementState !== undefined) { + if (typeof options.emailPasswordEnforcementState !== 'undefined') { if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -1865,7 +1918,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } - if (typeof options.phoneEnforcementState !== undefined) { + if (typeof options.phoneEnforcementState !== 'undefined') { if (!validator.isNonEmptyString(options.phoneEnforcementState)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -1905,6 +1958,38 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { ); } } + + if (typeof options.useSmsBotScore !== 'undefined') { + if (!validator.isBoolean(options.useSmsBotScore)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.useSmsBotScore" must be a boolean value".', + ); + } + } + + if (typeof options.useSmsTollFraudProtection !== 'undefined') { + if (!validator.isBoolean(options.useSmsTollFraudProtection)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".', + ); + } + } + + if (typeof options.tollFraudManagedRules !== 'undefined') { + // Validate array + if (!validator.isArray(options.tollFraudManagedRules)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".', + ); + } + // Validate each rule of the array + options.tollFraudManagedRules.forEach((tollFraudManagedRule) => { + RecaptchaAuthConfig.validateTollFraudManagedRule(tollFraudManagedRule); + }); + } } /** @@ -1942,6 +2027,41 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } + /** + * Validate each element in TollFraudManagedRule array + * @param options - The options object to validate. + */ + private static validateTollFraudManagedRule(options: RecaptchaTollFraudManagedRule): void { + const validKeys = { + startScore: true, + action: true, + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaTollFraudManagedRule" must be a non-null object.', + ); + } + // Check for unsupported top level attributes. + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid RecaptchaTollFraudManagedRule parameter.`, + ); + } + } + + // Validate content. + if (typeof options.action !== 'undefined' && + options.action !== 'BLOCK') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaTollFraudManagedRule.action" must be "BLOCK".', + ); + } + } + /** * Returns a JSON-serializable representation of this object. * @returns The JSON-serializable object representation of the ReCaptcha config instance @@ -1953,6 +2073,9 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { managedRules: deepCopy(this.managedRules), recaptchaKeys: deepCopy(this.recaptchaKeys), useAccountDefender: this.useAccountDefender, + useSmsBotScore: this.useSmsBotScore, + useSmsTollFraudProtection: this.useSmsTollFraudProtection, + tollFraudManagedRules: deepCopy(this.tollFraudManagedRules), } if (typeof json.emailPasswordEnforcementState === 'undefined') { @@ -1967,11 +2090,18 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof json.recaptchaKeys === 'undefined') { delete json.recaptchaKeys; } - if (typeof json.useAccountDefender === 'undefined') { delete json.useAccountDefender; } - + if (typeof json.useSmsBotScore === 'undefined') { + delete json.useSmsBotScore; + } + if (typeof json.useSmsTollFraudProtection === 'undefined') { + delete json.useSmsTollFraudProtection; + } + if (typeof json.tollFraudManagedRules === 'undefined') { + delete json.tollFraudManagedRules; + } return json; } } diff --git a/src/auth/index.ts b/src/auth/index.ts index f350b28837..52538eb7d5 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -89,6 +89,7 @@ export { RecaptchaKey, RecaptchaKeyClientType, RecaptchaManagedRule, + RecaptchaTollFraudManagedRule, RecaptchaProviderEnforcementState, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 250d6549ac..27f750b214 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -112,7 +112,12 @@ export class ProjectConfig { * {@link https://cloud.google.com/terms/service-terms | Term of Service}. */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; - + /** + * The reCAPTCHA configuration. + */ + get recaptchaConfig(): RecaptchaConfig | undefined { + return this.recaptchaConfig_; + } /** * The password policy configuration for the project */ @@ -203,12 +208,6 @@ export class ProjectConfig { return request; } - /** - * The reCAPTCHA configuration. - */ - get recaptchaConfig(): RecaptchaConfig | undefined { - return this.recaptchaConfig_; - } /** * The Project Config object constructor. * @@ -245,7 +244,7 @@ export class ProjectConfig { const json = { smsRegionConfig: deepCopy(this.smsRegionConfig), multiFactorConfig: deepCopy(this.multiFactorConfig), - recaptchaConfig: this.recaptchaConfig_?.toJSON(), + recaptchaConfig: deepCopy(this.recaptchaConfig), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), }; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7b113b3156..605a39527b 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,7 +32,7 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig, + PasswordPolicyConfig, SmsRegionConfig, RecaptchaConfig, } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; @@ -1269,30 +1269,44 @@ describe('admin.auth', () => { allowedRegions: ['AC', 'AD'], } }; + const recaptchaStateAuditConfig: RecaptchaConfig = { + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + tollFraudManagedRules: [ + { + startScore: 0.1, + action: 'BLOCK', + }, + ], + }; + const recaptchaStateOffConfig: RecaptchaConfig = { + emailPasswordEnforcementState: 'OFF', + phoneEnforcementState: 'OFF', + useAccountDefender: false, + useSmsBotScore: false, + useSmsTollFraudProtection: false, + }; const projectConfigOption1: UpdateProjectConfigRequest = { smsRegionConfig: smsRegionAllowByDefaultConfig, multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, passwordPolicyConfig: passwordConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'AUDIT', - managedRules: [ - { - endScore: 0.1, - action: 'BLOCK', - }, - ], - useAccountDefender: true, - }, + recaptchaConfig: recaptchaStateAuditConfig, emailPrivacyConfig: { enableImprovedEmailPrivacy: true, } }; const projectConfigOption2: UpdateProjectConfigRequest = { smsRegionConfig: smsRegionAllowlistOnlyConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'OFF', - useAccountDefender: false, - }, + recaptchaConfig: recaptchaStateOffConfig, emailPrivacyConfig: { enableImprovedEmailPrivacy: false, } @@ -1305,16 +1319,7 @@ describe('admin.auth', () => { smsRegionConfig: smsRegionAllowByDefaultConfig, multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, passwordPolicyConfig: passwordConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'AUDIT', - managedRules: [ - { - endScore: 0.1, - action: 'BLOCK', - }, - ], - useAccountDefender: true, - }, + recaptchaConfig: recaptchaStateAuditConfig, emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, @@ -1331,6 +1336,9 @@ describe('admin.auth', () => { action: 'BLOCK', }, ], + useAccountDefender: false, + useSmsBotScore: false, + useSmsTollFraudProtection: false, }, emailPrivacyConfig: {}, }; @@ -1338,15 +1346,7 @@ describe('admin.auth', () => { smsRegionConfig: smsRegionAllowlistOnlyConfig, multiFactorConfig: mfaSmsEnabledTotpDisabledConfig, passwordPolicyConfig: passwordConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'OFF', - managedRules: [ - { - endScore: 0.1, - action: 'BLOCK', - }, - ], - }, + recaptchaConfig: recaptchaStateOffConfig, emailPrivacyConfig: {}, }; @@ -1416,6 +1416,32 @@ describe('admin.auth', () => { disallowedRegions: ['AC', 'AD'], } } + const recaptchaStateAuditConfig: RecaptchaConfig = { + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + tollFraudManagedRules: [ + { + startScore: 0.1, + action: 'BLOCK', + }, + ], + } + const recaptchaStateOffConfig: RecaptchaConfig = { + emailPasswordEnforcementState: 'OFF', + phoneEnforcementState: 'OFF', + useAccountDefender: false, + useSmsBotScore: false, + useSmsTollFraudProtection: false, + } const tenantOptions: CreateTenantRequest = { displayName: 'testTenant1', emailSignInConfig: { @@ -1466,16 +1492,7 @@ describe('admin.auth', () => { testPhoneNumbers: { '+16505551234': '123456', }, - recaptchaConfig: { - emailPasswordEnforcementState: 'AUDIT', - managedRules: [ - { - endScore: 0.3, - action: 'BLOCK', - }, - ], - useAccountDefender: true, - }, + recaptchaConfig: recaptchaStateAuditConfig, emailPrivacyConfig: {}, }; const expectedUpdatedTenant2: any = { @@ -1487,16 +1504,7 @@ describe('admin.auth', () => { anonymousSignInEnabled: false, multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, smsRegionConfig: smsRegionAllowByDefaultConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'OFF', - managedRules: [ - { - endScore: 0.3, - action: 'BLOCK', - }, - ], - useAccountDefender: false, - }, + recaptchaConfig: recaptchaStateOffConfig, emailPrivacyConfig: {}, }; const expectedUpdatedTenantSmsEnabledTotpDisabled: any = { @@ -1508,16 +1516,7 @@ describe('admin.auth', () => { anonymousSignInEnabled: false, multiFactorConfig: mfaSmsEnabledTotpDisabledConfig, smsRegionConfig: smsRegionAllowByDefaultConfig, - recaptchaConfig: { - emailPasswordEnforcementState: 'OFF', - managedRules: [ - { - endScore: 0.3, - action: 'BLOCK', - }, - ], - useAccountDefender: false, - }, + recaptchaConfig: recaptchaStateOffConfig, emailPrivacyConfig: {}, }; diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 5934dd15fd..043e70b752 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -20,7 +20,6 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { RecaptchaAuthConfig } from '../../../src/auth/auth-config'; import { ProjectConfig, ProjectConfigServerResponse, @@ -70,6 +69,25 @@ describe('ProjectConfig', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + } }; const updateProjectConfigRequest1: UpdateProjectConfigRequest = { @@ -114,26 +132,40 @@ describe('ProjectConfig', () => { }, recaptchaConfig: { emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', managedRules: [ { endScore: 0.2, action: 'BLOCK' } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], recaptchaKeys: [ { type: 'WEB', key: 'test-key-1' } ], useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, } }; - const updateProjectConfigRequest: UpdateProjectConfigRequest = { + const updateProjectConfigRequest4: UpdateProjectConfigRequest = { recaptchaConfig: { emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', managedRules: [ { endScore: 0.2, action: 'BLOCK' } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, } }; @@ -206,7 +238,7 @@ describe('ProjectConfig', () => { }).not.to.throw; }); it('should throw on null RecaptchaConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig = null; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -214,15 +246,32 @@ describe('ProjectConfig', () => { }); it('should throw on invalid RecaptchaConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); + it('should throw on null phoneEnforcementState attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.phoneEnforcementState = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid phoneEnforcementState attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig + .phoneEnforcementState = 'INVALID'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + it('should throw on null emailPasswordEnforcementState attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -230,7 +279,7 @@ describe('ProjectConfig', () => { }); it('should throw on invalid emailPasswordEnforcementState attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig .emailPasswordEnforcementState = 'INVALID'; expect(() => { @@ -241,16 +290,38 @@ describe('ProjectConfig', () => { const invalidUseAccountDefender = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidUseAccountDefender.forEach((useAccountDefender) => { it(`should throw given invalid useAccountDefender parameter: ${JSON.stringify(useAccountDefender)}`, () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); }); }); + + const invalidUseSmsBotScore = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsBotScore.forEach((useSmsBotScore) => { + it(`should throw given invalid useSmsBotScore parameter: ${JSON.stringify(useSmsBotScore)}`, () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.useSmsBotScore = useSmsBotScore; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.useSmsBotScore" must be a boolean value".'); + }); + }); + + const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { + it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".'); + }); + }); it('should throw on non-array managedRules attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -258,7 +329,7 @@ describe('ProjectConfig', () => { }); it('should throw on invalid managedRules attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { @@ -267,13 +338,39 @@ describe('ProjectConfig', () => { }); it('should throw on invalid RecaptchaManagedRule.action attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); + + it('should throw on non-array tollFraudManagedRules attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }); + + it('should throw on invalid tollFraudManagedRules attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = + [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"score" is not a valid RecaptchaTollFraudManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; + configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = + [{ 'startScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaTollFraudManagedRule.action" must be "BLOCK".'); + }); it('should throw on null PasswordPolicyConfig attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; @@ -284,145 +381,145 @@ describe('ProjectConfig', () => { }); it('should throw on invalid PasswordPolicyConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.invalidParameter = 'invalid', + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.invalidParameter = 'invalid', expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig parameter.'); }); it('should throw on missing enforcementState', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - delete tenantOptionsClientRequest.passwordPolicyConfig.enforcementState; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + delete configOptionsClientRequest.passwordPolicyConfig.enforcementState; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); }); it('should throw on invalid enforcementState', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.enforcementState = 'INVALID_STATE'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.enforcementState = 'INVALID_STATE'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); }); it('should throw on invalid forceUpgradeOnSignin', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.forceUpgradeOnSignin = 'INVALID'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.forceUpgradeOnSignin = 'INVALID'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.'); }); it('should throw on undefined constraints when state is enforced', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - delete tenantOptionsClientRequest.passwordPolicyConfig.constraints; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + delete configOptionsClientRequest.passwordPolicyConfig.constraints; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); }); it('should throw on invalid constraints attribute', ()=> { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig.constraints parameter.'); }); it('should throw on null constraints object', ()=> { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints = null; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints = null; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); }); it('should throw on invalid constraints object', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); }); it('should throw on invalid uppercase type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireUppercase = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.requireUppercase = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.requireUppercase"' + ' must be a boolean.'); }); it('should throw on invalid lowercase type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireLowercase = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.requireLowercase = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.requireLowercase"' + ' must be a boolean.'); }); it('should throw on invalid numeric type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNumeric = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.requireNumeric = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.requireNumeric"' + ' must be a boolean.'); }); it('should throw on invalid non-alphanumeric type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNonAlphanumeric = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.requireNonAlphanumeric = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + ' must be a boolean.'); }); it('should throw on invalid minLength type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.minLength" must be a number.'); }); it('should throw on invalid maxLength type', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 'invalid'; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 'invalid'; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.maxLength" must be a number.'); }); it('should throw on invalid minLength range', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.'); }); it('should throw on invalid maxLength range', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 5000; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 5000; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + ' must be greater than or equal to minLength and at max 4096.'); }); it('should throw if minLength is greater than maxLength', () => { - const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 20; - tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 7; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 20; + configOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 7; expect(() => { - ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + ProjectConfig.buildServerRequest(configOptionsClientRequest); }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + ' must be greater than or equal to minLength and at max 4096.'); }); @@ -461,7 +558,7 @@ describe('ProjectConfig', () => { }); it('should throw on unsupported attribute for update request', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.unsupported = 'value'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -490,6 +587,7 @@ describe('ProjectConfig', () => { it('should set readonly property multiFactorConfig', () => { const expectedMultiFactorConfig = { state: 'DISABLED', + factorIds: [], providerConfigs: [ { state: 'ENABLED', @@ -503,20 +601,25 @@ describe('ProjectConfig', () => { }); it('should set readonly property recaptchaConfig', () => { - const expectedRecaptchaConfig = new RecaptchaAuthConfig( - { - emailPasswordEnforcementState: 'AUDIT', - managedRules: [ { - endScore: 0.2, - action: 'BLOCK' - } ], - recaptchaKeys: [ { - type: 'WEB', - key: 'test-key-1' } - ], - useAccountDefender: true, - } - ); + const expectedRecaptchaConfig = { + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + }; expect(projectConfig.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); }); @@ -548,11 +651,57 @@ describe('ProjectConfig', () => { const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); it('should return the expected object representation of project config', () => { expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ - smsRegionConfig: deepCopy(serverResponse.smsRegionConfig), - multiFactorConfig: deepCopy(serverResponse.mfa), - recaptchaConfig: deepCopy(serverResponse.recaptchaConfig), - passwordPolicyConfig: deepCopy(serverResponse.passwordPolicyConfig), - emailPrivacyConfig: deepCopy(serverResponse.emailPrivacyConfig), + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + multiFactorConfig: { + state: 'DISABLED', + factorIds: [], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, + }, + passwordPolicyConfig: { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + requireUppercase: true, + minLength: 8, + maxLength: 30, + }, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }); }); @@ -563,7 +712,10 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.emailPasswordEnforcementState; delete serverResponseOptionalCopy.recaptchaConfig?.managedRules; delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; - delete serverResponseOptionalCopy.passwordPolicyConfig; + delete serverResponseOptionalCopy.recaptchaConfig?.useSmsBotScore; + delete serverResponseOptionalCopy.recaptchaConfig?.phoneEnforcementState; + delete serverResponseOptionalCopy.recaptchaConfig?.tollFraudManagedRules; + delete serverResponseOptionalCopy.recaptchaConfig?.useSmsTollFraudProtection delete serverResponseOptionalCopy.passwordPolicyConfig; delete serverResponseOptionalCopy.emailPrivacyConfig; expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 5050ed1332..cb3a904d06 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -172,12 +172,19 @@ describe('Tenant', () => { '+16505550676': '985235', }, recaptchaConfig: { - managedRules: [{ + emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', + managedRules: [ { endScore: 0.2, action: 'BLOCK' - }], - emailPasswordEnforcementState: 'AUDIT', + } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, }, }; @@ -204,15 +211,22 @@ describe('Tenant', () => { }, recaptchaConfig: { emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', managedRules: [ { endScore: 0.2, action: 'BLOCK' } ], + tollFraudManagedRules: [ { + startScore: 0.1, + action: 'BLOCK' + } ], recaptchaKeys: [ { type: 'WEB', key: 'test-key-1' } ], useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, }, smsRegionConfig: smsAllowByDefault, passwordPolicyConfig: passwordPolicyServerConfig, @@ -313,6 +327,40 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); + + const invalidUseAccountDefender = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseAccountDefender.forEach((useAccountDefender) => { + it(`should throw given invalid useAccountDefender parameter: ${JSON.stringify(useAccountDefender)}`, () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); + }); + }); + + const invalidUseSmsBotScore = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsBotScore.forEach((useSmsBotScore) => { + it(`should throw given invalid useSmsBotScore parameter: ${JSON.stringify(useSmsBotScore)}`, () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useSmsBotScore = useSmsBotScore; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.useSmsBotScore" must be a boolean value".'); + }); + }); + + const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { + it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".'); + }); + }); + it('should throw on non-array managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; @@ -321,32 +369,46 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); }); - - it('should throw on non-boolean useAccountDefender attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = 'yes'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); - }); - + it('should throw on invalid managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.managedRules = - [{ 'score': 0.1, 'action': 'BLOCK' }]; + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); }); - + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.managedRules = - [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); + + it('should throw on non-array tollFraudManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }); + + it('should throw on invalid tollFraudManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"score" is not a valid RecaptchaTollFraudManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaTollFraudManagedRule.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'startScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaTollFraudManagedRule.action" must be "BLOCK".'); + }); it('should throw on invalid testPhoneNumbers attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; @@ -721,18 +783,10 @@ describe('Tenant', () => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); - - it('should throw on non-array managedRules attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); - }); - + const invalidUseAccountDefender = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidUseAccountDefender.forEach((useAccountDefender) => { - it('should throw on non-boolean useAccountDefender attribute', () => { + it(`should throw given invalid useAccountDefender parameter: ${JSON.stringify(useAccountDefender)}`, () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender; expect(() => { @@ -740,24 +794,77 @@ describe('Tenant', () => { }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); }); }); + + const invalidUseSmsBotScore = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsBotScore.forEach((useSmsBotScore) => { + it(`should throw given invalid useSmsBotScore parameter: ${JSON.stringify(useSmsBotScore)}`, () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useSmsBotScore = useSmsBotScore; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.useSmsBotScore" must be a boolean value".'); + }); + }); + + const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { + it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".'); + }); + }); + + it('should throw on non-array managedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); + }); + it('should throw on invalid managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.managedRules = - [{ 'score': 0.1, 'action': 'BLOCK' }]; + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); }); - + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.managedRules = - [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); + + it('should throw on non-array tollFraudManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }); + + it('should throw on invalid tollFraudManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"score" is not a valid RecaptchaTollFraudManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaTollFraudManagedRule.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'startScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaTollFraudManagedRule.action" must be "BLOCK".'); + }); it('should throw on invalid testPhoneNumbers attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; @@ -1104,15 +1211,22 @@ describe('Tenant', () => { const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy); const expectedRecaptchaConfig = new RecaptchaAuthConfig({ emailPasswordEnforcementState: 'AUDIT', + phoneEnforcementState: 'AUDIT', managedRules: [{ endScore: 0.2, action: 'BLOCK' }], + tollFraudManagedRules: [{ + startScore: 0.1, + action: 'BLOCK', + }], recaptchaKeys: [ { type: 'WEB', key: 'test-key-1' } ], useAccountDefender: true, + useSmsBotScore: true, + useSmsTollFraudProtection: true, }); expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); }); From 8a16df74f86747e2c8d077105f8a036a2893e766 Mon Sep 17 00:00:00 2001 From: Pragati Date: Mon, 29 Jul 2024 15:21:41 -0700 Subject: [PATCH 5/8] lint fixes and api extractor --- etc/firebase-admin.auth.api.md | 10 ++++++++++ test/unit/auth/project-config.spec.ts | 6 ++++-- test/unit/auth/tenant.spec.ts | 12 ++++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 2b581a0a77..3ca52862fa 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -879,7 +879,11 @@ export interface RecaptchaConfig { managedRules?: RecaptchaManagedRule[]; phoneEnforcementState?: RecaptchaProviderEnforcementState; recaptchaKeys?: RecaptchaKey[]; + tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; useAccountDefender?: boolean; + useSmsBotScore?: boolean; + // (undocumented) + useSmsTollFraudProtection?: boolean; } // @public @@ -900,6 +904,12 @@ export interface RecaptchaManagedRule { // @public export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; +// @public +export interface RecaptchaTollFraudManagedRule { + action?: RecaptchaAction; + startScore: number; +} + // @public export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { callbackURL?: string; diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 043e70b752..192fe3a03a 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -311,7 +311,8 @@ describe('ProjectConfig', () => { const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { - it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + it(`should throw given invalid useSmsTollFraudProtection parameter: ' + + '${JSON.stringify(useSmsTollFraudProtection)}`, () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; configOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; expect(() => { @@ -351,7 +352,8 @@ describe('ProjectConfig', () => { configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid ' + + '"RecaptchaTollFraudManagedRule".'); }); it('should throw on invalid tollFraudManagedRules attribute', () => { diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index cb3a904d06..5777214572 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -352,7 +352,8 @@ describe('Tenant', () => { const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { - it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + it(`should throw given invalid useSmsTollFraudProtection parameter:' + + '${JSON.stringify(useSmsTollFraudProtection)}`, () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; expect(() => { @@ -391,7 +392,8 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid '+ + '"RecaptchaTollFraudManagedRule".'); }); it('should throw on invalid tollFraudManagedRules attribute', () => { @@ -808,7 +810,8 @@ describe('Tenant', () => { const invalidUseSmsTollFraudProtection = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidUseSmsTollFraudProtection.forEach((useSmsTollFraudProtection) => { - it(`should throw given invalid useSmsTollFraudProtection parameter: ${JSON.stringify(useSmsTollFraudProtection)}`, () => { + it(`should throw given invalid useSmsTollFraudProtection parameter:' + + ' ${JSON.stringify(useSmsTollFraudProtection)}`, () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.useSmsTollFraudProtection = useSmsTollFraudProtection; expect(() => { @@ -847,7 +850,8 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".'); + }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid' + + ' "RecaptchaTollFraudManagedRule".'); }); it('should throw on invalid tollFraudManagedRules attribute', () => { From 4a674d710798e8ab8aec06ebb72df287e19c5d98 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 5 Sep 2024 11:29:08 -0700 Subject: [PATCH 6/8] restructure with auth/client naming --- etc/firebase-admin.auth.api.md | 2 +- src/auth/auth-config.ts | 159 +++++++++++++++----------- src/auth/project-config.ts | 7 +- src/auth/tenant.ts | 14 +-- test/integration/auth.spec.ts | 4 +- test/unit/auth/project-config.spec.ts | 16 +-- test/unit/auth/tenant.spec.ts | 33 +++--- 7 files changed, 134 insertions(+), 101 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 3ca52862fa..035530be27 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -879,7 +879,7 @@ export interface RecaptchaConfig { managedRules?: RecaptchaManagedRule[]; phoneEnforcementState?: RecaptchaProviderEnforcementState; recaptchaKeys?: RecaptchaKey[]; - tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; + smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; useAccountDefender?: boolean; useSmsBotScore?: boolean; // (undocumented) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 8531ec9103..7efcb53d78 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1756,7 +1756,7 @@ export interface RecaptchaManagedRule { */ export interface RecaptchaTollFraudManagedRule { /** - * The action will be enforced if the reCAPTCHA score of a request is larger than endScore. + * The action will be enforced if the reCAPTCHA score of a request is larger than startScore. */ startScore: number; /** @@ -1827,9 +1827,29 @@ export interface RecaptchaConfig { * The managed rules for toll fraud provider, containing the enforcement status. * The toll fraud provider contains all SMS related user flows. */ + smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; +} + +/** + * Server side recaptcha configuration. + */ +export interface RecaptchaAuthServerConfig { + emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + phoneEnforcementState?: RecaptchaProviderEnforcementState; + managedRules?: RecaptchaManagedRule[]; + recaptchaKeys?: RecaptchaKey[]; + useAccountDefender?: boolean; + useSmsBotScore?: boolean; + useSmsTollFraudProtection?: boolean; tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; } +/** + * Defines the recaptcha config class used to convert client side RecaptchaConfig + * to a format that is understood by the Auth server. + * + * @internal + */ export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; public readonly phoneEnforcementState?: RecaptchaProviderEnforcementState; @@ -1838,34 +1858,82 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly useAccountDefender?: boolean; public readonly useSmsBotScore?: boolean; public readonly useSmsTollFraudProtection?: boolean; - public readonly tollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; + public readonly smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; - constructor(recaptchaConfig: RecaptchaConfig) { - if (typeof recaptchaConfig.emailPasswordEnforcementState !== 'undefined') { - this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; + + /** + * The RecaptchaAuthConfig constructor. + * + * @param response - The server side response used to initialize the + * RecaptchaAuthConfig object. + * @constructor + * @internal + */ + constructor(response: RecaptchaAuthServerConfig) { + if (typeof response.emailPasswordEnforcementState !== 'undefined') { + this.emailPasswordEnforcementState = response.emailPasswordEnforcementState; } - if (typeof recaptchaConfig.phoneEnforcementState !== 'undefined') { - this.phoneEnforcementState = recaptchaConfig.phoneEnforcementState; + if (typeof response.phoneEnforcementState !== 'undefined') { + this.phoneEnforcementState = response.phoneEnforcementState; } - if (typeof recaptchaConfig.managedRules !== 'undefined') { - this.managedRules = recaptchaConfig.managedRules; + if (typeof response.managedRules !== 'undefined') { + this.managedRules = response.managedRules; } - if (typeof recaptchaConfig.recaptchaKeys !== 'undefined') { - this.recaptchaKeys = recaptchaConfig.recaptchaKeys; + if (typeof response.recaptchaKeys !== 'undefined') { + this.recaptchaKeys = response.recaptchaKeys; } - if (typeof recaptchaConfig.useAccountDefender !== 'undefined') { - this.useAccountDefender = recaptchaConfig.useAccountDefender; + if (typeof response.useAccountDefender !== 'undefined') { + this.useAccountDefender = response.useAccountDefender; } - if (typeof recaptchaConfig.useSmsBotScore !== 'undefined') { - this.useSmsBotScore = recaptchaConfig.useSmsBotScore; + if (typeof response.useSmsBotScore !== 'undefined') { + this.useSmsBotScore = response.useSmsBotScore; } - if (typeof recaptchaConfig.useSmsTollFraudProtection !== 'undefined') { - this.useSmsTollFraudProtection = recaptchaConfig.useSmsTollFraudProtection; + if (typeof response.useSmsTollFraudProtection !== 'undefined') { + this.useSmsTollFraudProtection = response.useSmsTollFraudProtection; } - if (typeof recaptchaConfig.tollFraudManagedRules !== 'undefined') { - this.tollFraudManagedRules = recaptchaConfig.tollFraudManagedRules; + if (typeof response.tollFraudManagedRules !== 'undefined') { + this.smsTollFraudManagedRules = response.tollFraudManagedRules; } } + + /** + * Builds a server request object from the client-side RecaptchaConfig. + * Converts client-side fields to their server-side equivalents. + * + * @param options - The client-side RecaptchaConfig object. + * @returns The server-side RecaptchaAuthServerConfig object. + */ + public static buildServerRequest(options: RecaptchaConfig): RecaptchaAuthServerConfig { + RecaptchaAuthConfig.validate(options); // Validate options before building request + + const request: RecaptchaAuthServerConfig = {}; + + if (typeof options.emailPasswordEnforcementState !== 'undefined') { + request.emailPasswordEnforcementState = options.emailPasswordEnforcementState; + } + if (typeof options.phoneEnforcementState !== 'undefined') { + request.phoneEnforcementState = options.phoneEnforcementState; + } + if (typeof options.managedRules !== 'undefined') { + request.managedRules = options.managedRules; + } + if (typeof options.recaptchaKeys !== 'undefined') { + request.recaptchaKeys = options.recaptchaKeys; + } + if (typeof options.useAccountDefender !== 'undefined') { + request.useAccountDefender = options.useAccountDefender; + } + if (typeof options.useSmsBotScore !== 'undefined') { + request.useSmsBotScore = options.useSmsBotScore; + } + if (typeof options.useSmsTollFraudProtection !== 'undefined') { + request.useSmsTollFraudProtection = options.useSmsTollFraudProtection; + } + if (typeof options.smsTollFraudManagedRules !== 'undefined') { + request.tollFraudManagedRules = options.smsTollFraudManagedRules; // Map client-side field to server-side + } + return request; + } /** * Validates the RecaptchaConfig options object. Throws an error on failure. @@ -1880,7 +1948,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { useAccountDefender: true, useSmsBotScore: true, useSmsTollFraudProtection: true, - tollFraudManagedRules: true, + smsTollFraudManagedRules: true, }; if (!validator.isNonNullObject(options)) { @@ -1977,16 +2045,16 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } } - if (typeof options.tollFraudManagedRules !== 'undefined') { + if (typeof options.smsTollFraudManagedRules !== 'undefined') { // Validate array - if (!validator.isArray(options.tollFraudManagedRules)) { + if (!validator.isArray(options.smsTollFraudManagedRules)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, - '"RecaptchaConfig.tollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".', + '"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".', ); } // Validate each rule of the array - options.tollFraudManagedRules.forEach((tollFraudManagedRule) => { + options.smsTollFraudManagedRules.forEach((tollFraudManagedRule) => { RecaptchaAuthConfig.validateTollFraudManagedRule(tollFraudManagedRule); }); } @@ -2061,49 +2129,6 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { ); } } - - /** - * Returns a JSON-serializable representation of this object. - * @returns The JSON-serializable object representation of the ReCaptcha config instance - */ - public toJSON(): object { - const json: any = { - emailPasswordEnforcementState: this.emailPasswordEnforcementState, - phoneEnforcementState: this.phoneEnforcementState, - managedRules: deepCopy(this.managedRules), - recaptchaKeys: deepCopy(this.recaptchaKeys), - useAccountDefender: this.useAccountDefender, - useSmsBotScore: this.useSmsBotScore, - useSmsTollFraudProtection: this.useSmsTollFraudProtection, - tollFraudManagedRules: deepCopy(this.tollFraudManagedRules), - } - - if (typeof json.emailPasswordEnforcementState === 'undefined') { - delete json.emailPasswordEnforcementState; - } - if (typeof json.phoneEnforcementState === 'undefined') { - delete json.phoneEnforcementState; - } - if (typeof json.managedRules === 'undefined') { - delete json.managedRules; - } - if (typeof json.recaptchaKeys === 'undefined') { - delete json.recaptchaKeys; - } - if (typeof json.useAccountDefender === 'undefined') { - delete json.useAccountDefender; - } - if (typeof json.useSmsBotScore === 'undefined') { - delete json.useSmsBotScore; - } - if (typeof json.useSmsTollFraudProtection === 'undefined') { - delete json.useSmsTollFraudProtection; - } - if (typeof json.tollFraudManagedRules === 'undefined') { - delete json.tollFraudManagedRules; - } - return json; - } } /** diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 27f750b214..4a40e0c012 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -23,6 +23,7 @@ import { MultiFactorAuthServerConfig, RecaptchaConfig, RecaptchaAuthConfig, + RecaptchaAuthServerConfig, PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, PasswordPolicyConfig, @@ -67,7 +68,7 @@ export interface UpdateProjectConfigRequest { export interface ProjectConfigServerResponse { smsRegionConfig?: SmsRegionConfig; mfa?: MultiFactorAuthServerConfig; - recaptchaConfig?: RecaptchaConfig; + recaptchaConfig?: RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; } @@ -78,7 +79,7 @@ export interface ProjectConfigServerResponse { export interface ProjectConfigClientRequest { smsRegionConfig?: SmsRegionConfig; mfa?: MultiFactorAuthServerConfig; - recaptchaConfig?: RecaptchaConfig; + recaptchaConfig?: RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; } @@ -197,7 +198,7 @@ export class ProjectConfig { request.mfa = MultiFactorAuthConfig.buildServerRequest(configOptions.multiFactorConfig); } if (typeof configOptions.recaptchaConfig !== 'undefined') { - request.recaptchaConfig = configOptions.recaptchaConfig; + request.recaptchaConfig = RecaptchaAuthConfig.buildServerRequest(configOptions.recaptchaConfig); } if (typeof configOptions.passwordPolicyConfig !== 'undefined') { request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(configOptions.passwordPolicyConfig); diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 76d97f3259..4b66cace3e 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,8 +21,8 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig, - PasswordPolicyConfig, + MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig, + RecaptchaAuthServerConfig, PasswordPolicyConfig, PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, } from './auth-config'; @@ -92,7 +92,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; smsRegionConfig?: SmsRegionConfig; - recaptchaConfig?: RecaptchaConfig; + recaptchaConfig?: RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; } @@ -107,7 +107,7 @@ export interface TenantServerResponse { mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; smsRegionConfig?: SmsRegionConfig; - recaptchaConfig? : RecaptchaConfig; + recaptchaConfig? : RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; } @@ -209,7 +209,7 @@ export class Tenant { request.smsRegionConfig = tenantOptions.smsRegionConfig; } if (typeof tenantOptions.recaptchaConfig !== 'undefined') { - request.recaptchaConfig = tenantOptions.recaptchaConfig; + request.recaptchaConfig = RecaptchaAuthConfig.buildServerRequest(tenantOptions.recaptchaConfig); } if (typeof tenantOptions.passwordPolicyConfig !== 'undefined') { request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(tenantOptions.passwordPolicyConfig); @@ -306,7 +306,7 @@ export class Tenant { } // Validate reCAPTCHAConfig type if provided. if (typeof request.recaptchaConfig !== 'undefined') { - RecaptchaAuthConfig.validate(request.recaptchaConfig); + RecaptchaAuthConfig.buildServerRequest(request.recaptchaConfig); } // Validate passwordPolicyConfig type if provided. if (typeof request.passwordPolicyConfig !== 'undefined') { @@ -400,7 +400,7 @@ export class Tenant { anonymousSignInEnabled: this.anonymousSignInEnabled, testPhoneNumbers: this.testPhoneNumbers, smsRegionConfig: deepCopy(this.smsRegionConfig), - recaptchaConfig: this.recaptchaConfig_?.toJSON(), + recaptchaConfig: deepCopy(this.recaptchaConfig), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), }; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 605a39527b..3f60f92d14 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1281,7 +1281,7 @@ describe('admin.auth', () => { useAccountDefender: true, useSmsBotScore: true, useSmsTollFraudProtection: true, - tollFraudManagedRules: [ + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK', @@ -1428,7 +1428,7 @@ describe('admin.auth', () => { useAccountDefender: true, useSmsBotScore: true, useSmsTollFraudProtection: true, - tollFraudManagedRules: [ + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK', diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 192fe3a03a..922f4dbe83 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -159,7 +159,7 @@ describe('ProjectConfig', () => { endScore: 0.2, action: 'BLOCK' } ], - tollFraudManagedRules: [ { + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK' } ], @@ -347,18 +347,18 @@ describe('ProjectConfig', () => { }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); - it('should throw on non-array tollFraudManagedRules attribute', () => { + it('should throw on non-array smsTollFraudManagedRules attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; - configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + configOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = 'non-array'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid ' + + }).to.throw('"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid ' + '"RecaptchaTollFraudManagedRule".'); }); - it('should throw on invalid tollFraudManagedRules attribute', () => { + it('should throw on invalid smsTollFraudManagedRules attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; - configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = + configOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -367,7 +367,7 @@ describe('ProjectConfig', () => { it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest4) as any; - configOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = + configOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = [{ 'startScore': 0.1, 'action': 'ALLOW' }]; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); @@ -716,7 +716,7 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; delete serverResponseOptionalCopy.recaptchaConfig?.useSmsBotScore; delete serverResponseOptionalCopy.recaptchaConfig?.phoneEnforcementState; - delete serverResponseOptionalCopy.recaptchaConfig?.tollFraudManagedRules; + delete serverResponseOptionalCopy.recaptchaConfig?.smsTollFraudManagedRules; delete serverResponseOptionalCopy.recaptchaConfig?.useSmsTollFraudProtection delete serverResponseOptionalCopy.passwordPolicyConfig; delete serverResponseOptionalCopy.emailPrivacyConfig; diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 5777214572..05c8bbf6aa 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -174,11 +174,16 @@ describe('Tenant', () => { recaptchaConfig: { emailPasswordEnforcementState: 'AUDIT', phoneEnforcementState: 'AUDIT', + recaptchaKeys: [ { + key: 'test-key-1', + type: 'WEB' + } + ], managedRules: [ { endScore: 0.2, action: 'BLOCK' } ], - tollFraudManagedRules: [ { + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK' } ], @@ -387,18 +392,18 @@ describe('Tenant', () => { }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); - it('should throw on non-array tollFraudManagedRules attribute', () => { + it('should throw on non-array smsTollFraudManagedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid '+ + }).to.throw('"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid '+ '"RecaptchaTollFraudManagedRule".'); }); it('should throw on invalid tollFraudManagedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"score" is not a valid RecaptchaTollFraudManagedRule parameter.'); @@ -406,7 +411,8 @@ describe('Tenant', () => { it('should throw on invalid RecaptchaTollFraudManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'startScore': 0.1, 'action': 'ALLOW' }]; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = + [{ 'startScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"RecaptchaTollFraudManagedRule.action" must be "BLOCK".'); @@ -845,18 +851,18 @@ describe('Tenant', () => { }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); - it('should throw on non-array tollFraudManagedRules attribute', () => { + it('should throw on non-array smsTollFraudManagedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = 'non-array'; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RecaptchaConfig.tollFraudManagedRules" must be an array of valid' + + }).to.throw('"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid' + ' "RecaptchaTollFraudManagedRule".'); }); - it('should throw on invalid tollFraudManagedRules attribute', () => { + it('should throw on invalid smsTollFraudManagedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"score" is not a valid RecaptchaTollFraudManagedRule parameter.'); @@ -864,7 +870,8 @@ describe('Tenant', () => { it('should throw on invalid RecaptchaTollFraudManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.tollFraudManagedRules = [{ 'startScore': 0.1, 'action': 'ALLOW' }]; + tenantOptionsClientRequest.recaptchaConfig.smsTollFraudManagedRules = + [{ 'startScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"RecaptchaTollFraudManagedRule.action" must be "BLOCK".'); @@ -1296,7 +1303,7 @@ describe('Tenant', () => { multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), - recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig), + recaptchaConfig: deepCopy(clientRequestWithRecaptcha.recaptchaConfig), passwordPolicyConfig: deepCopy(clientRequest.passwordPolicyConfig), emailPrivacyConfig: deepCopy(clientRequest.emailPrivacyConfig), }); From f4761e22c90d75a4ec2a4159f6ec0614a977be68 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 5 Sep 2024 11:34:52 -0700 Subject: [PATCH 7/8] fixes --- test/unit/auth/project-config.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 922f4dbe83..f116647568 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -610,7 +610,7 @@ describe('ProjectConfig', () => { endScore: 0.2, action: 'BLOCK' } ], - tollFraudManagedRules: [ { + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK' } ], @@ -677,7 +677,7 @@ describe('ProjectConfig', () => { endScore: 0.2, action: 'BLOCK' } ], - tollFraudManagedRules: [ { + smsTollFraudManagedRules: [ { startScore: 0.1, action: 'BLOCK' } ], @@ -716,7 +716,7 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; delete serverResponseOptionalCopy.recaptchaConfig?.useSmsBotScore; delete serverResponseOptionalCopy.recaptchaConfig?.phoneEnforcementState; - delete serverResponseOptionalCopy.recaptchaConfig?.smsTollFraudManagedRules; + delete serverResponseOptionalCopy.recaptchaConfig?.tollFraudManagedRules; delete serverResponseOptionalCopy.recaptchaConfig?.useSmsTollFraudProtection delete serverResponseOptionalCopy.passwordPolicyConfig; delete serverResponseOptionalCopy.emailPrivacyConfig; From c0617b4f87457329b846fe35a765cb3866f34212 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 5 Sep 2024 15:29:01 -0700 Subject: [PATCH 8/8] address PR feedback --- etc/firebase-admin.auth.api.md | 1 - src/auth/auth-config.ts | 38 ++++++++++++---------------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 035530be27..593b4e674d 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -882,7 +882,6 @@ export interface RecaptchaConfig { smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[]; useAccountDefender?: boolean; useSmsBotScore?: boolean; - // (undocumented) useSmsTollFraudProtection?: boolean; } diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 7efcb53d78..4d0f273708 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1818,7 +1818,7 @@ export interface RecaptchaConfig { * Can only be true when the phone_enforcement_state is AUDIT or ENFORCE. */ useSmsBotScore?: boolean; - /* + /** * Whether to use the rCE sms toll fraud protection risk score for reCAPTCHA phone provider. * Can only be true when the phone_enforcement_state is AUDIT or ENFORCE. */ @@ -1870,31 +1870,19 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { * @internal */ constructor(response: RecaptchaAuthServerConfig) { - if (typeof response.emailPasswordEnforcementState !== 'undefined') { - this.emailPasswordEnforcementState = response.emailPasswordEnforcementState; - } - if (typeof response.phoneEnforcementState !== 'undefined') { - this.phoneEnforcementState = response.phoneEnforcementState; - } - if (typeof response.managedRules !== 'undefined') { - this.managedRules = response.managedRules; - } - if (typeof response.recaptchaKeys !== 'undefined') { - this.recaptchaKeys = response.recaptchaKeys; - } - if (typeof response.useAccountDefender !== 'undefined') { - this.useAccountDefender = response.useAccountDefender; - } - if (typeof response.useSmsBotScore !== 'undefined') { - this.useSmsBotScore = response.useSmsBotScore; - } - if (typeof response.useSmsTollFraudProtection !== 'undefined') { - this.useSmsTollFraudProtection = response.useSmsTollFraudProtection; - } - if (typeof response.tollFraudManagedRules !== 'undefined') { - this.smsTollFraudManagedRules = response.tollFraudManagedRules; + const filteredResponse = Object.fromEntries( + Object.entries(response).filter(([, value]) => value !== undefined) + ); + + // Explicitly map the 'tollFraudManagedRules' to 'smsTollFraudManagedRules' + if (filteredResponse.tollFraudManagedRules !== undefined) { + this.smsTollFraudManagedRules = filteredResponse.tollFraudManagedRules; + delete filteredResponse.tollFraudManagedRules; // Remove it if necessary } - } + + // Assign the remaining properties directly + Object.assign(this, filteredResponse); + } /** * Builds a server request object from the client-side RecaptchaConfig.