Skip to content

Commit

Permalink
feat(auth): Add recaptcha and sms toll fraud support for phone auth (#…
Browse files Browse the repository at this point in the history
…2625)

* enable recaptcha enterprise on phone auth

* undo autosave editor changes

* remove editor whitespace changes

* fix unit tests + add sms tf changes

* lint fixes and api extractor

* restructure with auth/client naming

* fixes

* address PR feedback

* Update src/auth/auth-config.ts

Co-authored-by: Kevin Cheung <[email protected]>

* fixing rCE phone enablement integ tests on project level

* fix rCE phone support Integration Tests

---------

Co-authored-by: Liubin Jiang <[email protected]>
Co-authored-by: Kevin Cheung <[email protected]>
Co-authored-by: Liubin Jiang <[email protected]>
  • Loading branch information
4 people authored Oct 22, 2024
1 parent f1c5523 commit 58b1a36
Show file tree
Hide file tree
Showing 8 changed files with 762 additions and 229 deletions.
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

0 comments on commit 58b1a36

Please sign in to comment.