diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md
index c7090af304..aa3c047867 100644
--- a/etc/firebase-admin.auth.api.md
+++ b/etc/firebase-admin.auth.api.md
@@ -336,6 +336,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
// @public
export class ProjectConfig {
+ get recaptchaConfig(): RecaptchaConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}
@@ -354,6 +355,35 @@ export interface ProviderIdentifier {
providerUid: string;
}
+// @public
+export type RecaptchaAction = 'BLOCK';
+
+// @public
+export interface RecaptchaConfig {
+ emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
+ managedRules?: RecaptchaManagedRule[];
+ recaptchaKeys?: RecaptchaKey[];
+ useAccountDefender?: boolean;
+}
+
+// @public
+export interface RecaptchaKey {
+ key: string;
+ type?: RecaptchaKeyClientType;
+}
+
+// @public
+export type RecaptchaKeyClientType = 'WEB';
+
+// @public
+export interface RecaptchaManagedRule {
+ action?: RecaptchaAction;
+ endScore: number;
+}
+
+// @public
+export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
+
// @public
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
callbackURL?: string;
@@ -389,6 +419,7 @@ export class Tenant {
readonly displayName?: string;
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
+ get recaptchaConfig(): RecaptchaConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
readonly tenantId: string;
readonly testPhoneNumbers?: {
@@ -434,6 +465,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
// @public
export interface UpdateProjectConfigRequest {
+ recaptchaConfig?: RecaptchaConfig;
smsRegionConfig?: SmsRegionConfig;
}
@@ -457,6 +489,7 @@ export interface UpdateTenantRequest {
displayName?: string;
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
+ recaptchaConfig?: RecaptchaConfig;
smsRegionConfig?: SmsRegionConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts
index 45ca3ef2d0..cb10daa2f3 100644
--- a/src/auth/auth-config.ts
+++ b/src/auth/auth-config.ts
@@ -1594,3 +1594,227 @@ export class SmsRegionsAuthConfig {
}
}
}
+/**
+* Enforcement state of reCAPTCHA protection.
+* - 'OFF': Unenforced.
+* - 'AUDIT': Create assessment but don't enforce the result.
+* - 'ENFORCE': Create assessment and enforce the result.
+*/
+export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
+
+/**
+* The actions to take for reCAPTCHA-protected requests.
+* - 'BLOCK': The reCAPTCHA-protected request will be blocked.
+*/
+export type RecaptchaAction = 'BLOCK';
+
+/**
+ * The config for a reCAPTCHA action rule.
+ */
+export interface RecaptchaManagedRule {
+ /**
+ * The action will be enforced if the reCAPTCHA score of a request is larger than endScore.
+ */
+ endScore: number;
+ /**
+ * The action for reCAPTCHA-protected requests.
+ */
+ action?: RecaptchaAction;
+}
+
+/**
+ * The key's platform type: only web is currently supported.
+ */
+export type RecaptchaKeyClientType = 'WEB';
+
+/**
+ * The reCAPTCHA key config.
+ */
+export interface RecaptchaKey {
+ /**
+ * The key's client platform type.
+ */
+ type?: RecaptchaKeyClientType;
+
+ /**
+ * The reCAPTCHA site key.
+ */
+ key: string;
+}
+
+/**
+ * The request interface for updating a reCAPTCHA Config.
+ * By enabling reCAPTCHA Enterprise Integration you are
+ * agreeing to reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+export interface RecaptchaConfig {
+ /**
+ * The enforcement state of the email password provider.
+ */
+ emailPasswordEnforcementState?: 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;
+}
+
+export class RecaptchaAuthConfig implements RecaptchaConfig {
+ public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
+ public readonly managedRules?: RecaptchaManagedRule[];
+ public readonly recaptchaKeys?: RecaptchaKey[];
+ public readonly useAccountDefender?: boolean;
+
+ constructor(recaptchaConfig: RecaptchaConfig) {
+ this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
+ this.managedRules = recaptchaConfig.managedRules;
+ this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
+ this.useAccountDefender = recaptchaConfig.useAccountDefender;
+ }
+
+ /**
+ * Validates the RecaptchaConfig options object. Throws an error on failure.
+ * @param options - The options object to validate.
+ */
+ public static validate(options: RecaptchaConfig): void {
+ const validKeys = {
+ emailPasswordEnforcementState: true,
+ managedRules: true,
+ recaptchaKeys: true,
+ useAccountDefender: true,
+ };
+
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig" must be a non-null object.',
+ );
+ }
+
+ for (const key in options) {
+ if (!(key in validKeys)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ `"${key}" is not a valid RecaptchaConfig parameter.`,
+ );
+ }
+ }
+
+ // Validation
+ if (typeof options.emailPasswordEnforcementState !== undefined) {
+ if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_ARGUMENT,
+ '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
+ );
+ }
+
+ if (options.emailPasswordEnforcementState !== 'OFF' &&
+ options.emailPasswordEnforcementState !== 'AUDIT' &&
+ options.emailPasswordEnforcementState !== 'ENFORCE') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
+ );
+ }
+ }
+
+ if (typeof options.managedRules !== 'undefined') {
+ // Validate array
+ if (!validator.isArray(options.managedRules)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
+ );
+ }
+ // Validate each rule of the array
+ options.managedRules.forEach((managedRule) => {
+ RecaptchaAuthConfig.validateManagedRule(managedRule);
+ });
+ }
+
+ if (typeof options.useAccountDefender != 'undefined') {
+ if (!validator.isBoolean(options.useAccountDefender)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaConfig.useAccountDefender" must be a boolean value".',
+ );
+ }
+ }
+ }
+
+ /**
+ * Validate each element in ManagedRule array
+ * @param options - The options object to validate.
+ */
+ private static validateManagedRule(options: RecaptchaManagedRule): void {
+ const validKeys = {
+ endScore: true,
+ action: true,
+ }
+ if (!validator.isNonNullObject(options)) {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaManagedRule" 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 RecaptchaManagedRule parameter.`,
+ );
+ }
+ }
+
+ // Validate content.
+ if (typeof options.action !== 'undefined' &&
+ options.action !== 'BLOCK') {
+ throw new FirebaseAuthError(
+ AuthClientErrorCode.INVALID_CONFIG,
+ '"RecaptchaManagedRule.action" must be "BLOCK".',
+ );
+ }
+ }
+
+ /**
+ * 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,
+ managedRules: deepCopy(this.managedRules),
+ recaptchaKeys: deepCopy(this.recaptchaKeys),
+ useAccountDefender: this.useAccountDefender,
+ }
+
+ if (typeof json.emailPasswordEnforcementState === 'undefined') {
+ delete json.emailPasswordEnforcementState;
+ }
+ if (typeof json.managedRules === 'undefined') {
+ delete json.managedRules;
+ }
+ if (typeof json.recaptchaKeys === 'undefined') {
+ delete json.recaptchaKeys;
+ }
+
+ if (typeof json.useAccountDefender === 'undefined') {
+ delete json.useAccountDefender;
+ }
+
+ return json;
+ }
+}
diff --git a/src/auth/index.ts b/src/auth/index.ts
index 7dec658473..7be7fe8c65 100644
--- a/src/auth/index.ts
+++ b/src/auth/index.ts
@@ -83,6 +83,12 @@ export {
OAuthResponseType,
OIDCAuthProviderConfig,
OIDCUpdateAuthProviderRequest,
+ RecaptchaAction,
+ RecaptchaConfig,
+ RecaptchaKey,
+ RecaptchaKeyClientType,
+ RecaptchaManagedRule,
+ RecaptchaProviderEnforcementState,
SAMLAuthProviderConfig,
SAMLUpdateAuthProviderRequest,
SmsRegionConfig,
diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts
index 030b64a779..847aa7d982 100644
--- a/src/auth/project-config-manager.ts
+++ b/src/auth/project-config-manager.ts
@@ -20,14 +20,10 @@ import {
} from './auth-api-request';
/**
- * Defines the project config manager used to help manage project config related operations.
- * This includes:
- *
- * - The ability to update and get project config.
+ * Manages (gets and updates) the current project config.
*/
export class ProjectConfigManager {
private readonly authRequestHandler: AuthRequestHandler;
-
/**
* Initializes a ProjectConfigManager instance for a specified FirebaseApp.
*
diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts
index 54dcfb3b9c..46cb55cf53 100644
--- a/src/auth/project-config.ts
+++ b/src/auth/project-config.ts
@@ -18,6 +18,8 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
SmsRegionsAuthConfig,
SmsRegionConfig,
+ RecaptchaConfig,
+ RecaptchaAuthConfig,
} from './auth-config';
import { deepCopy } from '../utils/deep-copy';
@@ -29,22 +31,31 @@ export interface UpdateProjectConfigRequest {
* The SMS configuration to update on the project.
*/
smsRegionConfig?: SmsRegionConfig;
+ /**
+ * The reCAPTCHA configuration to update on the project.
+ * By enabling reCAPTCHA Enterprise integration, you are
+ * agreeing to the reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+ recaptchaConfig?: RecaptchaConfig;
}
/**
- * Response received from getting or updating a project config.
- * This object currently exposes only the SMS Region config.
+ * Response received when getting or updating the project config.
+ * Currently only includes the reCAPTCHA and SMS Region config.
*/
export interface ProjectConfigServerResponse {
smsRegionConfig?: SmsRegionConfig;
+ recaptchaConfig?: RecaptchaConfig;
}
/**
- * Request sent to update project config.
- * This object currently exposes only the SMS Region config.
+ * Request to update the project config.
+ * Currently only includes the reCAPTCHA and SMS Region config.
*/
export interface ProjectConfigClientRequest {
smsRegionConfig?: SmsRegionConfig;
+ recaptchaConfig?: RecaptchaConfig;
}
/**
@@ -57,6 +68,13 @@ export class ProjectConfig {
* This is based on the calling code of the destination phone number.
*/
public readonly smsRegionConfig?: SmsRegionConfig;
+ /**
+ * The reCAPTCHA configuration to update on the project.
+ * By enabling reCAPTCHA Enterprise integration, you are
+ * agreeing to the reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+ private readonly recaptchaConfig_?: RecaptchaAuthConfig;
/**
* Validates a project config options object. Throws an error on failure.
@@ -72,6 +90,7 @@ export class ProjectConfig {
}
const validKeys = {
smsRegionConfig: true,
+ recaptchaConfig: true,
}
// Check for unsupported top level attributes.
for (const key in request) {
@@ -86,20 +105,31 @@ export class ProjectConfig {
if (typeof request.smsRegionConfig !== 'undefined') {
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
}
+
+ // Validate reCAPTCHA config attribute.
+ if (typeof request.recaptchaConfig !== 'undefined') {
+ RecaptchaAuthConfig.validate(request.recaptchaConfig);
+ }
}
/**
* Build the corresponding server request for a UpdateProjectConfigRequest object.
* @param configOptions - The properties to convert to a server request.
* @returns The equivalent server request.
- *
+ *
* @internal
*/
public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest {
- ProjectConfig.validate(configOptions);
+ ProjectConfig.validate(configOptions);
return configOptions as ProjectConfigClientRequest;
}
-
+
+ /**
+ * The reCAPTCHA configuration.
+ */
+ get recaptchaConfig(): RecaptchaConfig | undefined {
+ return this.recaptchaConfig_;
+ }
/**
* The Project Config object constructor.
*
@@ -111,6 +141,9 @@ export class ProjectConfig {
if (typeof response.smsRegionConfig !== 'undefined') {
this.smsRegionConfig = response.smsRegionConfig;
}
+ if (typeof response.recaptchaConfig !== 'undefined') {
+ this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
+ }
}
/**
* Returns a JSON-serializable representation of this object.
@@ -121,10 +154,14 @@ export class ProjectConfig {
// JSON serialization
const json = {
smsRegionConfig: deepCopy(this.smsRegionConfig),
+ recaptchaConfig: this.recaptchaConfig_?.toJSON(),
};
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
+ if (typeof json.recaptchaConfig === 'undefined') {
+ delete json.recaptchaConfig;
+ }
return json;
}
}
diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts
index 56cf2abd8d..fdb7b1e199 100644
--- a/src/auth/tenant.ts
+++ b/src/auth/tenant.ts
@@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig,
MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig,
- MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig
+ MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig
} from './auth-config';
/**
@@ -59,6 +59,14 @@ export interface UpdateTenantRequest {
* The SMS configuration to update on the project.
*/
smsRegionConfig?: SmsRegionConfig;
+
+ /**
+ * The reCAPTCHA configuration to update on the tenant.
+ * By enabling reCAPTCHA Enterprise integration, you are
+ * agreeing to the reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+ recaptchaConfig?: RecaptchaConfig;
}
/**
@@ -74,6 +82,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
smsRegionConfig?: SmsRegionConfig;
+ recaptchaConfig?: RecaptchaConfig;
}
/** The tenant server response interface. */
@@ -86,6 +95,7 @@ export interface TenantServerResponse {
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
smsRegionConfig?: SmsRegionConfig;
+ recaptchaConfig? : RecaptchaConfig;
}
/**
@@ -130,6 +140,13 @@ export class Tenant {
private readonly emailSignInConfig_?: EmailSignInConfig;
private readonly multiFactorConfig_?: MultiFactorAuthConfig;
+ /**
+ * The map conatining the reCAPTCHA config.
+ * By enabling reCAPTCHA Enterprise Integration you are
+ * agreeing to reCAPTCHA Enterprise
+ * {@link https://cloud.google.com/terms/service-terms | Term of Service}.
+ */
+ private readonly recaptchaConfig_?: RecaptchaAuthConfig;
/**
* The SMS Regions Config to update a tenant.
* Configures the regions where users are allowed to send verification SMS.
@@ -169,6 +186,9 @@ export class Tenant {
if (typeof tenantOptions.smsRegionConfig !== 'undefined') {
request.smsRegionConfig = tenantOptions.smsRegionConfig;
}
+ if (typeof tenantOptions.recaptchaConfig !== 'undefined') {
+ request.recaptchaConfig = tenantOptions.recaptchaConfig;
+ }
return request;
}
@@ -203,6 +223,7 @@ export class Tenant {
multiFactorConfig: true,
testPhoneNumbers: true,
smsRegionConfig: true,
+ recaptchaConfig: true,
};
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
if (!validator.isNonNullObject(request)) {
@@ -253,6 +274,10 @@ export class Tenant {
if (typeof request.smsRegionConfig != 'undefined') {
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
}
+ // Validate reCAPTCHAConfig type if provided.
+ if (typeof request.recaptchaConfig !== 'undefined') {
+ RecaptchaAuthConfig.validate(request.recaptchaConfig);
+ }
}
/**
@@ -290,6 +315,9 @@ export class Tenant {
if (typeof response.smsRegionConfig !== 'undefined') {
this.smsRegionConfig = deepCopy(response.smsRegionConfig);
}
+ if (typeof response.recaptchaConfig !== 'undefined') {
+ this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
+ }
}
/**
@@ -306,6 +334,13 @@ export class Tenant {
return this.multiFactorConfig_;
}
+ /**
+ * The recaptcha config auth configuration of the current tenant.
+ */
+ get recaptchaConfig(): RecaptchaConfig | undefined {
+ return this.recaptchaConfig_;
+ }
+
/**
* Returns a JSON-serializable representation of this object.
*
@@ -320,6 +355,7 @@ export class Tenant {
anonymousSignInEnabled: this.anonymousSignInEnabled,
testPhoneNumbers: this.testPhoneNumbers,
smsRegionConfig: deepCopy(this.smsRegionConfig),
+ recaptchaConfig: this.recaptchaConfig_?.toJSON(),
};
if (typeof json.multiFactorConfig === 'undefined') {
delete json.multiFactorConfig;
@@ -330,6 +366,9 @@ export class Tenant {
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
+ if (typeof json.recaptchaConfig === 'undefined') {
+ delete json.recaptchaConfig;
+ }
return json;
}
}
diff --git a/src/utils/error.ts b/src/utils/error.ts
index 6c74748ed1..cdb7faef05 100644
--- a/src/utils/error.ts
+++ b/src/utils/error.ts
@@ -737,6 +737,18 @@ export class AuthClientErrorCode {
code: 'user-not-disabled',
message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).',
};
+ public static INVALID_RECAPTCHA_ACTION = {
+ code: 'invalid-recaptcha-action',
+ message: 'reCAPTCHA action must be "BLOCK".'
+ }
+ public static INVALID_RECAPTCHA_ENFORCEMENT_STATE = {
+ code: 'invalid-recaptcha-enforcement-state',
+ message: 'reCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".'
+ }
+ public static RECAPTCHA_NOT_ENABLED = {
+ code: 'racaptcha-not-enabled',
+ message: 'reCAPTCHA enterprise is not enabled.'
+ }
}
/**
@@ -996,6 +1008,12 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = {
USER_DISABLED: 'USER_DISABLED',
// Password provided is too weak.
WEAK_PASSWORD: 'INVALID_PASSWORD',
+ // Unrecognized reCAPTCHA action.
+ INVALID_RECAPTCHA_ACTION: 'INVALID_RECAPTCHA_ACTION',
+ // Unrecognized reCAPTCHA enforcement state.
+ INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'INVALID_RECAPTCHA_ENFORCEMENT_STATE',
+ // reCAPTCHA is not enabled for account defender.
+ RECAPTCHA_NOT_ENABLED: 'RECAPTCHA_NOT_ENABLED'
};
/** @const {ServerToClientCode} Messaging server to client enum error codes. */
diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts
index 68e66aaf06..53d1394a02 100644
--- a/test/integration/auth.spec.ts
+++ b/test/integration/auth.spec.ts
@@ -1212,6 +1212,11 @@ describe('admin.auth', () => {
disallowedRegions: ['AC', 'AD'],
}
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [{ endScore: 0.1, action: 'BLOCK' }],
+ useAccountDefender: true,
+ },
};
const projectConfigOption2: UpdateProjectConfigRequest = {
smsRegionConfig: {
@@ -1219,6 +1224,10 @@ describe('admin.auth', () => {
allowedRegions: ['AC', 'AD'],
}
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'OFF',
+ useAccountDefender: false,
+ },
};
const expectedProjectConfig1: any = {
smsRegionConfig: {
@@ -1226,6 +1235,11 @@ describe('admin.auth', () => {
disallowedRegions: ['AC', 'AD'],
}
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [{ endScore: 0.1, action: 'BLOCK' }],
+ useAccountDefender: true,
+ },
};
const expectedProjectConfig2: any = {
smsRegionConfig: {
@@ -1233,11 +1247,17 @@ describe('admin.auth', () => {
allowedRegions: ['AC', 'AD'],
}
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'OFF',
+ managedRules: [{ endScore: 0.1, action: 'BLOCK' }],
+ },
};
it('updateProjectConfig() should resolve with the updated project config', () => {
return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption1)
.then((actualProjectConfig) => {
+ // ReCAPTCHA keys are generated differently each time.
+ delete actualProjectConfig.recaptchaConfig?.recaptchaKeys;
expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig1);
return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption2);
})
@@ -1311,6 +1331,16 @@ describe('admin.auth', () => {
testPhoneNumbers: {
'+16505551234': '123456',
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [
+ {
+ endScore: 0.3,
+ action: 'BLOCK',
+ },
+ ],
+ useAccountDefender: true,
+ },
};
const expectedUpdatedTenant2: any = {
displayName: 'testTenantUpdated',
@@ -1328,6 +1358,16 @@ describe('admin.auth', () => {
disallowedRegions: ['AC', 'AD'],
}
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'OFF',
+ managedRules: [
+ {
+ endScore: 0.3,
+ action: 'BLOCK',
+ },
+ ],
+ useAccountDefender: false,
+ },
};
// https://mochajs.org/
@@ -1740,6 +1780,7 @@ describe('admin.auth', () => {
},
multiFactorConfig: deepCopy(expectedUpdatedTenant.multiFactorConfig),
testPhoneNumbers: deepCopy(expectedUpdatedTenant.testPhoneNumbers),
+ recaptchaConfig: deepCopy(expectedUpdatedTenant.recaptchaConfig),
};
const updatedOptions2: UpdateTenantRequest = {
emailSignInConfig: {
@@ -1750,6 +1791,7 @@ describe('admin.auth', () => {
// Test clearing of phone numbers.
testPhoneNumbers: null,
smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig),
+ recaptchaConfig: deepCopy(expectedUpdatedTenant2.recaptchaConfig),
};
if (authEmulatorHost) {
return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions)
@@ -1775,7 +1817,10 @@ describe('admin.auth', () => {
return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2);
})
.then((actualTenant) => {
- expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant2);
+ // response from backend ignores account defender status is recaptcha status is OFF.
+ const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2);
+ delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender;
+ expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy);
});
});
@@ -1797,7 +1842,35 @@ describe('admin.auth', () => {
}
return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2)
.then((actualTenant) => {
- expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant2);
+ // response from backend ignores account defender status is recaptcha status is OFF.
+ const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2);
+ delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender;
+ expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy);
+ });
+ });
+
+ it('updateTenant() should not update tenant reCAPTCHA config is undefined', () => {
+ expectedUpdatedTenant.tenantId = createdTenantId;
+ const updatedOptions2: UpdateTenantRequest = {
+ displayName: expectedUpdatedTenant2.displayName,
+ recaptchaConfig: undefined,
+ };
+ if (authEmulatorHost) {
+ return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2)
+ .then((actualTenant) => {
+ const actualTenantObj = actualTenant.toJSON();
+ // Not supported in Auth Emulator
+ delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers;
+ delete expectedUpdatedTenant2.testPhoneNumbers;
+ expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2);
+ });
+ }
+ return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2)
+ .then((actualTenant) => {
+ // response from backend ignores account defender status is recaptcha status is OFF.
+ const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2);
+ delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender;
+ expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy);
});
});
diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts
index d06b24fa80..3fc0770b36 100644
--- a/test/unit/auth/project-config-manager.spec.ts
+++ b/test/unit/auth/project-config-manager.spec.ts
@@ -51,6 +51,17 @@ describe('ProjectConfigManager', () => {
allowedRegions: [ 'AC', 'AD' ],
},
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [ {
+ endScore: 0.2,
+ action: 'BLOCK'
+ } ],
+ recaptchaKeys: [ {
+ type: 'WEB',
+ key: 'test-key-1' }
+ ],
+ }
};
before(() => {
@@ -131,6 +142,13 @@ describe('ProjectConfigManager', () => {
disallowedRegions: [ 'AC', 'AD' ],
},
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [ {
+ endScore: 0.2,
+ action: 'BLOCK'
+ } ],
+ }
};
const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE);
const expectedError = new FirebaseAuthError(
@@ -193,4 +211,4 @@ describe('ProjectConfigManager', () => {
});
});
});
-});
\ No newline at end of file
+});
diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts
index 19cc8f420d..28a8a18aae 100644
--- a/test/unit/auth/project-config.spec.ts
+++ b/test/unit/auth/project-config.spec.ts
@@ -20,6 +20,7 @@ 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,
@@ -66,6 +67,29 @@ describe('ProjectConfig', () => {
disallowedRegions: ['AC', 'AD'],
},
},
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [ {
+ endScore: 0.2,
+ action: 'BLOCK'
+ } ],
+ recaptchaKeys: [ {
+ type: 'WEB',
+ key: 'test-key-1' }
+ ],
+ useAccountDefender: true,
+ }
+ };
+
+ const updateProjectConfigRequest: UpdateProjectConfigRequest = {
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [ {
+ endScore: 0.2,
+ action: 'BLOCK'
+ } ],
+ useAccountDefender: true,
+ }
};
describe('buildServerRequest()', () => {
@@ -136,6 +160,75 @@ describe('ProjectConfig', () => {
ProjectConfig.buildServerRequest(configOptionsClientRequest2);
}).not.to.throw;
});
+ it('should throw on null RecaptchaConfig attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig = null;
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaConfig" must be a non-null object.');
+ });
+
+ it('should throw on invalid RecaptchaConfig attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid';
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.');
+ });
+
+ it('should throw on null emailPasswordEnforcementState attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null;
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.');
+ });
+
+ it('should throw on invalid emailPasswordEnforcementState attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig
+ .emailPasswordEnforcementState = 'INVALID';
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" 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 configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender;
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".');
+ });
+ });
+
+ it('should throw on non-array managedRules attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.managedRules = 'non-array';
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".');
+ });
+
+ it('should throw on invalid managedRules attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.managedRules =
+ [{ 'score': 0.1, 'action': 'BLOCK' }];
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.');
+ });
+
+ it('should throw on invalid RecaptchaManagedRule.action attribute', () => {
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
+ configOptionsClientRequest.recaptchaConfig.managedRules =
+ [{ 'endScore': 0.1, 'action': 'ALLOW' }];
+ expect(() => {
+ ProjectConfig.buildServerRequest(configOptionsClientRequest);
+ }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".');
+ });
const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop];
nonObjects.forEach((request) => {
@@ -147,7 +240,7 @@ describe('ProjectConfig', () => {
});
it('should throw on unsupported attribute for update request', () => {
- const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any;
+ const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.unsupported = 'value';
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
@@ -172,21 +265,46 @@ describe('ProjectConfig', () => {
};
expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig);
});
+ 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,
+ }
+ );
+ expect(projectConfig.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig);
+ });
});
describe('toJSON()', () => {
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)
+ smsRegionConfig: deepCopy(serverResponse.smsRegionConfig),
+ recaptchaConfig: deepCopy(serverResponse.recaptchaConfig)
});
});
it('should not populate optional fields if not available', () => {
const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse);
delete serverResponseOptionalCopy.smsRegionConfig;
+ delete serverResponseOptionalCopy.recaptchaConfig?.emailPasswordEnforcementState;
+ delete serverResponseOptionalCopy.recaptchaConfig?.managedRules;
+ delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender;
- expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({});
+ expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({
+ recaptchaConfig: {
+ recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys),
+ }
+ });
});
});
-});
\ No newline at end of file
+});
diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts
index 44885ecafa..dc64983069 100644
--- a/test/unit/auth/tenant.spec.ts
+++ b/test/unit/auth/tenant.spec.ts
@@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';
import { deepCopy } from '../../../src/utils/deep-copy';
-import { EmailSignInConfig, MultiFactorAuthConfig } from '../../../src/auth/auth-config';
+import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaAuthConfig } from '../../../src/auth/auth-config';
import { TenantServerResponse } from '../../../src/auth/tenant';
import {
CreateTenantRequest, UpdateTenantRequest, EmailSignInProviderConfig, Tenant,
@@ -93,6 +93,58 @@ describe('Tenant', () => {
},
};
+ const serverResponseWithRecaptcha: TenantServerResponse = {
+ name: 'projects/project1/tenants/TENANT-ID',
+ displayName: 'TENANT-DISPLAY-NAME',
+ allowPasswordSignup: true,
+ enableEmailLinkSignin: true,
+ mfaConfig: {
+ state: 'ENABLED',
+ enabledProviders: ['PHONE_SMS'],
+ },
+ testPhoneNumbers: {
+ '+16505551234': '019287',
+ '+16505550676': '985235',
+ },
+ recaptchaConfig: {
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [ {
+ endScore: 0.2,
+ action: 'BLOCK'
+ } ],
+ recaptchaKeys: [ {
+ type: 'WEB',
+ key: 'test-key-1' }
+ ],
+ useAccountDefender: true,
+ },
+ smsRegionConfig: smsAllowByDefault,
+ };
+
+ const clientRequestWithRecaptcha: UpdateTenantRequest = {
+ displayName: 'TENANT-DISPLAY-NAME',
+ emailSignInConfig: {
+ enabled: true,
+ passwordRequired: false,
+ },
+ multiFactorConfig: {
+ state: 'ENABLED',
+ factorIds: ['phone'],
+ },
+ testPhoneNumbers: {
+ '+16505551234': '019287',
+ '+16505550676': '985235',
+ },
+ recaptchaConfig: {
+ managedRules: [{
+ endScore: 0.2,
+ action: 'BLOCK'
+ }],
+ emailPasswordEnforcementState: 'AUDIT',
+ useAccountDefender: true,
+ },
+ };
+
describe('buildServerRequest()', () => {
const createRequest = true;
@@ -136,6 +188,73 @@ describe('Tenant', () => {
}).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".');
});
+ it('should throw on null RecaptchaConfig attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig = null;
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
+ }).to.throw('"RecaptchaConfig" must be a non-null object.');
+ });
+
+ it('should throw on invalid RecaptchaConfig attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid';
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
+ }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.');
+ });
+
+ it('should throw on null emailPasswordEnforcementState attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null;
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.');
+ });
+
+ it('should throw on invalid emailPasswordEnforcementState attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig
+ .emailPasswordEnforcementState = 'INVALID';
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" 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".');
+ });
+
+ 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' }];
+ 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' }];
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
+ }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".');
+ });
+
it('should throw on invalid testPhoneNumbers attribute', () => {
const tenantOptionsClientRequest = deepCopy(clientRequest) as any;
tenantOptionsClientRequest.testPhoneNumbers = 'invalid';
@@ -214,7 +333,7 @@ describe('Tenant', () => {
});
it('should not throw on valid client request object', () => {
- const tenantOptionsClientRequest = deepCopy(clientRequest);
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha);
expect(() => {
Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest);
}).not.to.throw;
@@ -284,6 +403,76 @@ describe('Tenant', () => {
}).to.throw('"invalid" is not a valid "AuthFactorType".',);
});
+ it('should throw on null RecaptchaConfig attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig = null;
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest);
+ }).to.throw('"RecaptchaConfig" must be a non-null object.');
+ });
+
+ it('should throw on invalid RecaptchaConfig attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid';
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest);
+ }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.');
+ });
+
+ it('should throw on null emailPasswordEnforcementState attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null;
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.');
+ });
+
+ it('should throw on invalid emailPasswordEnforcementState attribute', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig
+ .emailPasswordEnforcementState = 'INVALID';
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest);
+ }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" 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', () => {
+ const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any;
+ tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender;
+ 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' }];
+ 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' }];
+ expect(() => {
+ Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest);
+ }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".');
+ });
+
it('should throw on invalid testPhoneNumbers attribute', () => {
const tenantOptionsClientRequest = deepCopy(clientRequest) as any;
tenantOptionsClientRequest.testPhoneNumbers = { 'invalid': '123456' };
@@ -439,6 +628,25 @@ describe('Tenant', () => {
expect(tenant.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig);
});
+ it('should set readonly property recaptchaConfig', () => {
+ const serverRequestWithRecaptchaCopy: TenantServerResponse =
+ deepCopy(serverResponseWithRecaptcha);
+ const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy);
+ const expectedRecaptchaConfig = new RecaptchaAuthConfig({
+ emailPasswordEnforcementState: 'AUDIT',
+ managedRules: [{
+ endScore: 0.2,
+ action: 'BLOCK'
+ }],
+ recaptchaKeys: [ {
+ type: 'WEB',
+ key: 'test-key-1' }
+ ],
+ useAccountDefender: true,
+ });
+ expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig);
+ });
+
it('should set readonly property testPhoneNumbers', () => {
expect(tenant.testPhoneNumbers).to.deep.equal(
deepCopy(clientRequest.testPhoneNumbers));
@@ -475,7 +683,7 @@ describe('Tenant', () => {
});
describe('toJSON()', () => {
- const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest);
+ const serverRequestCopy: TenantServerResponse = deepCopy(serverResponseWithRecaptcha);
it('should return the expected object representation of a tenant', () => {
expect(new Tenant(serverRequestCopy).toJSON()).to.deep.equal({
tenantId: 'TENANT-ID',
@@ -488,14 +696,16 @@ describe('Tenant', () => {
multiFactorConfig: deepCopy(clientRequest.multiFactorConfig),
testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers),
smsRegionConfig: deepCopy(clientRequest.smsRegionConfig),
+ recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig),
});
});
it('should not populate optional fields if not available', () => {
- const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest);
+ const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverResponseWithRecaptcha);
delete serverRequestCopyWithoutMfa.mfaConfig;
delete serverRequestCopyWithoutMfa.testPhoneNumbers;
delete serverRequestCopyWithoutMfa.smsRegionConfig;
+ delete serverRequestCopyWithoutMfa.recaptchaConfig;
expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({
tenantId: 'TENANT-ID',