Skip to content

Commit 58b1a36

Browse files
pragatimodiXiaoshouzi-ghkevinthecheung
authored
feat(auth): Add recaptcha and sms toll fraud support for phone auth (#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]>
1 parent f1c5523 commit 58b1a36

File tree

8 files changed

+762
-229
lines changed

8 files changed

+762
-229
lines changed

etc/firebase-admin.auth.api.md

+10
Original file line numberDiff line numberDiff line change
@@ -877,8 +877,12 @@ export type RecaptchaAction = 'BLOCK';
877877
export interface RecaptchaConfig {
878878
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
879879
managedRules?: RecaptchaManagedRule[];
880+
phoneEnforcementState?: RecaptchaProviderEnforcementState;
880881
recaptchaKeys?: RecaptchaKey[];
882+
smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
881883
useAccountDefender?: boolean;
884+
useSmsBotScore?: boolean;
885+
useSmsTollFraudProtection?: boolean;
882886
}
883887

884888
// @public
@@ -899,6 +903,12 @@ export interface RecaptchaManagedRule {
899903
// @public
900904
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
901905

906+
// @public
907+
export interface RecaptchaTollFraudManagedRule {
908+
action?: RecaptchaAction;
909+
startScore: number;
910+
}
911+
902912
// @public
903913
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
904914
callbackURL?: string;

src/auth/auth-config.ts

+200-28
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,21 @@ export interface RecaptchaManagedRule {
17501750
action?: RecaptchaAction;
17511751
}
17521752

1753+
/**
1754+
* The managed rules for toll fraud provider, containing the enforcement status.
1755+
* The toll fraud provider contains all SMS related user flows.
1756+
*/
1757+
export interface RecaptchaTollFraudManagedRule {
1758+
/**
1759+
* The action will be enforced if the reCAPTCHA score of a request is larger than startScore.
1760+
*/
1761+
startScore: number;
1762+
/**
1763+
* The action for reCAPTCHA-protected requests.
1764+
*/
1765+
action?: RecaptchaAction;
1766+
}
1767+
17531768
/**
17541769
* The key's platform type.
17551770
*/
@@ -1781,34 +1796,131 @@ export interface RecaptchaConfig {
17811796
* The enforcement state of the email password provider.
17821797
*/
17831798
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1799+
/**
1800+
* The enforcement state of the phone provider.
1801+
*/
1802+
phoneEnforcementState?: RecaptchaProviderEnforcementState;
17841803
/**
17851804
* The reCAPTCHA managed rules.
17861805
*/
17871806
managedRules?: RecaptchaManagedRule[];
1788-
17891807
/**
17901808
* The reCAPTCHA keys.
17911809
*/
17921810
recaptchaKeys?: RecaptchaKey[];
1793-
17941811
/**
17951812
* Whether to use account defender for reCAPTCHA assessment.
17961813
* The default value is false.
17971814
*/
17981815
useAccountDefender?: boolean;
1816+
/**
1817+
* Whether to use the rCE bot score for reCAPTCHA phone provider.
1818+
* Can only be true when the phone_enforcement_state is AUDIT or ENFORCE.
1819+
*/
1820+
useSmsBotScore?: boolean;
1821+
/**
1822+
* Whether to use the rCE SMS toll fraud protection risk score for reCAPTCHA phone provider.
1823+
* Can only be true when the phone_enforcement_state is AUDIT or ENFORCE.
1824+
*/
1825+
useSmsTollFraudProtection?: boolean;
1826+
/**
1827+
* The managed rules for toll fraud provider, containing the enforcement status.
1828+
* The toll fraud provider contains all SMS related user flows.
1829+
*/
1830+
smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
1831+
}
1832+
1833+
/**
1834+
* Server side recaptcha configuration.
1835+
*/
1836+
export interface RecaptchaAuthServerConfig {
1837+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1838+
phoneEnforcementState?: RecaptchaProviderEnforcementState;
1839+
managedRules?: RecaptchaManagedRule[];
1840+
recaptchaKeys?: RecaptchaKey[];
1841+
useAccountDefender?: boolean;
1842+
useSmsBotScore?: boolean;
1843+
useSmsTollFraudProtection?: boolean;
1844+
tollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
17991845
}
18001846

1847+
/**
1848+
* Defines the recaptcha config class used to convert client side RecaptchaConfig
1849+
* to a format that is understood by the Auth server.
1850+
*
1851+
* @internal
1852+
*/
18011853
export class RecaptchaAuthConfig implements RecaptchaConfig {
18021854
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1855+
public readonly phoneEnforcementState?: RecaptchaProviderEnforcementState;
18031856
public readonly managedRules?: RecaptchaManagedRule[];
18041857
public readonly recaptchaKeys?: RecaptchaKey[];
18051858
public readonly useAccountDefender?: boolean;
1859+
public readonly useSmsBotScore?: boolean;
1860+
public readonly useSmsTollFraudProtection?: boolean;
1861+
public readonly smsTollFraudManagedRules?: RecaptchaTollFraudManagedRule[];
1862+
1863+
1864+
/**
1865+
* The RecaptchaAuthConfig constructor.
1866+
*
1867+
* @param response - The server side response used to initialize the
1868+
* RecaptchaAuthConfig object.
1869+
* @constructor
1870+
* @internal
1871+
*/
1872+
constructor(response: RecaptchaAuthServerConfig) {
1873+
const filteredResponse = Object.fromEntries(
1874+
Object.entries(response).filter(([, value]) => value !== undefined)
1875+
);
1876+
1877+
// Explicitly map the 'tollFraudManagedRules' to 'smsTollFraudManagedRules'
1878+
if (filteredResponse.tollFraudManagedRules !== undefined) {
1879+
this.smsTollFraudManagedRules = filteredResponse.tollFraudManagedRules;
1880+
delete filteredResponse.tollFraudManagedRules; // Remove it if necessary
1881+
}
1882+
1883+
// Assign the remaining properties directly
1884+
Object.assign(this, filteredResponse);
1885+
}
1886+
1887+
/**
1888+
* Builds a server request object from the client-side RecaptchaConfig.
1889+
* Converts client-side fields to their server-side equivalents.
1890+
*
1891+
* @param options - The client-side RecaptchaConfig object.
1892+
* @returns The server-side RecaptchaAuthServerConfig object.
1893+
*/
1894+
public static buildServerRequest(options: RecaptchaConfig): RecaptchaAuthServerConfig {
1895+
RecaptchaAuthConfig.validate(options); // Validate options before building request
1896+
1897+
const request: RecaptchaAuthServerConfig = {};
18061898

1807-
constructor(recaptchaConfig: RecaptchaConfig) {
1808-
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
1809-
this.managedRules = recaptchaConfig.managedRules;
1810-
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
1811-
this.useAccountDefender = recaptchaConfig.useAccountDefender;
1899+
if (typeof options.emailPasswordEnforcementState !== 'undefined') {
1900+
request.emailPasswordEnforcementState = options.emailPasswordEnforcementState;
1901+
}
1902+
if (typeof options.phoneEnforcementState !== 'undefined') {
1903+
request.phoneEnforcementState = options.phoneEnforcementState;
1904+
}
1905+
if (typeof options.managedRules !== 'undefined') {
1906+
request.managedRules = options.managedRules;
1907+
}
1908+
if (typeof options.recaptchaKeys !== 'undefined') {
1909+
request.recaptchaKeys = options.recaptchaKeys;
1910+
}
1911+
if (typeof options.useAccountDefender !== 'undefined') {
1912+
request.useAccountDefender = options.useAccountDefender;
1913+
}
1914+
if (typeof options.useSmsBotScore !== 'undefined') {
1915+
request.useSmsBotScore = options.useSmsBotScore;
1916+
}
1917+
if (typeof options.useSmsTollFraudProtection !== 'undefined') {
1918+
request.useSmsTollFraudProtection = options.useSmsTollFraudProtection;
1919+
}
1920+
if (typeof options.smsTollFraudManagedRules !== 'undefined') {
1921+
request.tollFraudManagedRules = options.smsTollFraudManagedRules; // Map client-side field to server-side
1922+
}
1923+
return request;
18121924
}
18131925

18141926
/**
@@ -1818,9 +1930,13 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
18181930
public static validate(options: RecaptchaConfig): void {
18191931
const validKeys = {
18201932
emailPasswordEnforcementState: true,
1933+
phoneEnforcementState: true,
18211934
managedRules: true,
18221935
recaptchaKeys: true,
18231936
useAccountDefender: true,
1937+
useSmsBotScore: true,
1938+
useSmsTollFraudProtection: true,
1939+
smsTollFraudManagedRules: true,
18241940
};
18251941

18261942
if (!validator.isNonNullObject(options)) {
@@ -1840,7 +1956,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
18401956
}
18411957

18421958
// Validation
1843-
if (typeof options.emailPasswordEnforcementState !== undefined) {
1959+
if (typeof options.emailPasswordEnforcementState !== 'undefined') {
18441960
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
18451961
throw new FirebaseAuthError(
18461962
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -1858,6 +1974,24 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
18581974
}
18591975
}
18601976

1977+
if (typeof options.phoneEnforcementState !== 'undefined') {
1978+
if (!validator.isNonEmptyString(options.phoneEnforcementState)) {
1979+
throw new FirebaseAuthError(
1980+
AuthClientErrorCode.INVALID_ARGUMENT,
1981+
'"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.',
1982+
);
1983+
}
1984+
1985+
if (options.phoneEnforcementState !== 'OFF' &&
1986+
options.phoneEnforcementState !== 'AUDIT' &&
1987+
options.phoneEnforcementState !== 'ENFORCE') {
1988+
throw new FirebaseAuthError(
1989+
AuthClientErrorCode.INVALID_CONFIG,
1990+
'"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
1991+
);
1992+
}
1993+
}
1994+
18611995
if (typeof options.managedRules !== 'undefined') {
18621996
// Validate array
18631997
if (!validator.isArray(options.managedRules)) {
@@ -1880,6 +2014,38 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
18802014
);
18812015
}
18822016
}
2017+
2018+
if (typeof options.useSmsBotScore !== 'undefined') {
2019+
if (!validator.isBoolean(options.useSmsBotScore)) {
2020+
throw new FirebaseAuthError(
2021+
AuthClientErrorCode.INVALID_CONFIG,
2022+
'"RecaptchaConfig.useSmsBotScore" must be a boolean value".',
2023+
);
2024+
}
2025+
}
2026+
2027+
if (typeof options.useSmsTollFraudProtection !== 'undefined') {
2028+
if (!validator.isBoolean(options.useSmsTollFraudProtection)) {
2029+
throw new FirebaseAuthError(
2030+
AuthClientErrorCode.INVALID_CONFIG,
2031+
'"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".',
2032+
);
2033+
}
2034+
}
2035+
2036+
if (typeof options.smsTollFraudManagedRules !== 'undefined') {
2037+
// Validate array
2038+
if (!validator.isArray(options.smsTollFraudManagedRules)) {
2039+
throw new FirebaseAuthError(
2040+
AuthClientErrorCode.INVALID_CONFIG,
2041+
'"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".',
2042+
);
2043+
}
2044+
// Validate each rule of the array
2045+
options.smsTollFraudManagedRules.forEach((tollFraudManagedRule) => {
2046+
RecaptchaAuthConfig.validateTollFraudManagedRule(tollFraudManagedRule);
2047+
});
2048+
}
18832049
}
18842050

18852051
/**
@@ -1918,32 +2084,38 @@ export class RecaptchaAuthConfig implements RecaptchaConfig {
19182084
}
19192085

19202086
/**
1921-
* Returns a JSON-serializable representation of this object.
1922-
* @returns The JSON-serializable object representation of the ReCaptcha config instance
2087+
* Validate each element in TollFraudManagedRule array
2088+
* @param options - The options object to validate.
19232089
*/
1924-
public toJSON(): object {
1925-
const json: any = {
1926-
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
1927-
managedRules: deepCopy(this.managedRules),
1928-
recaptchaKeys: deepCopy(this.recaptchaKeys),
1929-
useAccountDefender: this.useAccountDefender,
1930-
}
1931-
1932-
if (typeof json.emailPasswordEnforcementState === 'undefined') {
1933-
delete json.emailPasswordEnforcementState;
2090+
private static validateTollFraudManagedRule(options: RecaptchaTollFraudManagedRule): void {
2091+
const validKeys = {
2092+
startScore: true,
2093+
action: true,
19342094
}
1935-
if (typeof json.managedRules === 'undefined') {
1936-
delete json.managedRules;
2095+
if (!validator.isNonNullObject(options)) {
2096+
throw new FirebaseAuthError(
2097+
AuthClientErrorCode.INVALID_CONFIG,
2098+
'"RecaptchaTollFraudManagedRule" must be a non-null object.',
2099+
);
19372100
}
1938-
if (typeof json.recaptchaKeys === 'undefined') {
1939-
delete json.recaptchaKeys;
2101+
// Check for unsupported top level attributes.
2102+
for (const key in options) {
2103+
if (!(key in validKeys)) {
2104+
throw new FirebaseAuthError(
2105+
AuthClientErrorCode.INVALID_CONFIG,
2106+
`"${key}" is not a valid RecaptchaTollFraudManagedRule parameter.`,
2107+
);
2108+
}
19402109
}
19412110

1942-
if (typeof json.useAccountDefender === 'undefined') {
1943-
delete json.useAccountDefender;
2111+
// Validate content.
2112+
if (typeof options.action !== 'undefined' &&
2113+
options.action !== 'BLOCK') {
2114+
throw new FirebaseAuthError(
2115+
AuthClientErrorCode.INVALID_CONFIG,
2116+
'"RecaptchaTollFraudManagedRule.action" must be "BLOCK".',
2117+
);
19442118
}
1945-
1946-
return json;
19472119
}
19482120
}
19492121

src/auth/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export {
8989
RecaptchaKey,
9090
RecaptchaKeyClientType,
9191
RecaptchaManagedRule,
92+
RecaptchaTollFraudManagedRule,
9293
RecaptchaProviderEnforcementState,
9394
SAMLAuthProviderConfig,
9495
SAMLUpdateAuthProviderRequest,

0 commit comments

Comments
 (0)