Skip to content

feat(auth): Added reCAPTCHA and SMS toll fraud configuration support for Phone Authentication #2625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 22, 2024
10 changes: 10 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,8 +877,12 @@ export type RecaptchaAction = 'BLOCK';
export interface RecaptchaConfig {
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
managedRules?: RecaptchaManagedRule[];
phoneEnforcementState?: RecaptchaProviderEnforcementState;
recaptchaKeys?: RecaptchaKey[];
smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
useAccountDefender?: boolean;
useSmsBotScore?: boolean;
useSmsTollFraudProtection?: boolean;
}

// @public
Expand All @@ -899,6 +903,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;
Expand Down
228 changes: 200 additions & 28 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 startScore.
*/
startScore: number;
/**
* The action for reCAPTCHA-protected requests.
*/
action?: RecaptchaAction;
}

/**
* The key's platform type.
*/
Expand Down Expand Up @@ -1781,34 +1796,131 @@ 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.
*/
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.
*/
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;
public readonly managedRules?: RecaptchaManagedRule[];
public readonly recaptchaKeys?: RecaptchaKey[];
public readonly useAccountDefender?: boolean;
public readonly useSmsBotScore?: boolean;
public readonly useSmsTollFraudProtection?: boolean;
public readonly smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[];


/**
* The RecaptchaAuthConfig constructor.
*
* @param response - The server side response used to initialize the
* RecaptchaAuthConfig object.
* @constructor
* @internal
*/
constructor(response: RecaptchaAuthServerConfig) {
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.
* 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 = {};

constructor(recaptchaConfig: RecaptchaConfig) {
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
this.managedRules = recaptchaConfig.managedRules;
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
this.useAccountDefender = recaptchaConfig.useAccountDefender;
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;
}

/**
Expand All @@ -1818,9 +1930,13 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
public static validate(options: RecaptchaConfig): void {
const validKeys = {
emailPasswordEnforcementState: true,
phoneEnforcementState: true,
managedRules: true,
recaptchaKeys: true,
useAccountDefender: true,
useSmsBotScore: true,
useSmsTollFraudProtection: true,
smsTollFraudManagedRules: true,
};

if (!validator.isNonNullObject(options)) {
Expand All @@ -1840,7 +1956,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,
Expand All @@ -1858,6 +1974,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)) {
Expand All @@ -1880,6 +2014,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.smsTollFraudManagedRules !== 'undefined') {
// Validate array
if (!validator.isArray(options.smsTollFraudManagedRules)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".',
);
}
// Validate each rule of the array
options.smsTollFraudManagedRules.forEach((tollFraudManagedRule) => {
RecaptchaAuthConfig.validateTollFraudManagedRule(tollFraudManagedRule);
});
}
}

/**
Expand Down Expand Up @@ -1918,32 +2084,38 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
}

/**
* Returns a JSON-serializable representation of this object.
* @returns The JSON-serializable object representation of the ReCaptcha config instance
* Validate each element in TollFraudManagedRule array
* @param options - The options object to validate.
*/
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;
private static validateTollFraudManagedRule(options: RecaptchaTollFraudManagedRule): void {
const validKeys = {
startScore: true,
action: true,
}
if (typeof json.managedRules === 'undefined') {
delete json.managedRules;
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaTollFraudManagedRule" must be a non-null object.',
);
}
if (typeof json.recaptchaKeys === 'undefined') {
delete json.recaptchaKeys;
// 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.`,
);
}
}

if (typeof json.useAccountDefender === 'undefined') {
delete json.useAccountDefender;
// Validate content.
if (typeof options.action !== 'undefined' &&
options.action !== 'BLOCK') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaTollFraudManagedRule.action" must be "BLOCK".',
);
}

return json;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export {
RecaptchaKey,
RecaptchaKeyClientType,
RecaptchaManagedRule,
RecaptchaTollFraudManagedRule,
RecaptchaProviderEnforcementState,
SAMLAuthProviderConfig,
SAMLUpdateAuthProviderRequest,
Expand Down
Loading