Skip to content
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

feat(auth): Add recaptcha and sms toll fraud support for phone auth #2625

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
11 changes: 11 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,8 +877,13 @@ export type RecaptchaAction = 'BLOCK';
export interface RecaptchaConfig {
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
managedRules?: RecaptchaManagedRule[];
phoneEnforcementState?: RecaptchaProviderEnforcementState;
recaptchaKeys?: RecaptchaKey[];
tollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
useAccountDefender?: boolean;
useSmsBotScore?: boolean;
// (undocumented)
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
useSmsTollFraudProtection?: boolean;
}

// @public
Expand All @@ -899,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;
Expand Down
177 changes: 168 additions & 9 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 endScore.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be startScore instead of endScore in the doc? Also should we mention the range of values that is allowed and add that into our validation logic?

*/
startScore: number;
/**
* The action for reCAPTCHA-protected requests.
*/
action?: RecaptchaAction;
}

/**
* The key's platform type.
*/
Expand Down Expand Up @@ -1781,34 +1796,75 @@ 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;
/*
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
* 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[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a discrepancy between rpc request and response field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the rpc request address the field as tollFraudManagedRules and SDK exposes it as smsTollFraudManagedRules

}

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 tollFraudManagedRules?: RecaptchaTollFraudManagedRule[];

constructor(recaptchaConfig: RecaptchaConfig) {
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
this.managedRules = recaptchaConfig.managedRules;
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
this.useAccountDefender = recaptchaConfig.useAccountDefender;
if (typeof recaptchaConfig.emailPasswordEnforcementState !== 'undefined') {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}

/**
Expand All @@ -1818,9 +1874,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,
tollFraudManagedRules: true,
};

if (!validator.isNonNullObject(options)) {
Expand All @@ -1840,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,
Expand All @@ -1858,6 +1918,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 +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);
});
}
}

/**
Expand Down Expand Up @@ -1917,32 +2027,81 @@ 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
*/
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;
}
}
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
15 changes: 7 additions & 8 deletions src/auth/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -203,12 +208,6 @@ export class ProjectConfig {
return request;
}

/**
* The reCAPTCHA configuration.
*/
get recaptchaConfig(): RecaptchaConfig | undefined {
return this.recaptchaConfig_;
}
/**
* The Project Config object constructor.
*
Expand Down Expand Up @@ -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),
};
Expand Down
Loading