Skip to content

Commit 0bf7d29

Browse files
authoredApr 12, 2023
feat(auth): reCAPTCHA Public preview (#2129)
reCAPTCHA support * Defined reCAPTCHA config. (#1574) - Added reCAPTCHA protection states. - Added reCAPTCHA action rule. - Added reCAPTCHA key config. * Create/Update tenant with ReCAPTCHA Config (#1586) * Support reCaptcha config /create update on tenants. - Support create and update tenants with reCaptcha config. - Added reCaptcha unit tests on tenants operations. * Project config - Recaptcha config (#1595) * Recaptcha config changes in project config. - Implemented getProjectConfig. - Implemented updateProjectConfig. - Updated error code. - Add Term of Service consents. * Recapcha integ test (#1599) * Added integ test for Project Config and Tenants update on reCAPTCHA config * Account defender support for reCAPTCHA (#1616) * Support use_account_defender add-on feature for reCAPTCHA config. * Added integration test for account defender feature.
1 parent 0da72ef commit 0bf7d29

11 files changed

+845
-27
lines changed
 

‎etc/firebase-admin.auth.api.md

+33
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
344344
// @public
345345
export class ProjectConfig {
346346
get multiFactorConfig(): MultiFactorConfig | undefined;
347+
get recaptchaConfig(): RecaptchaConfig | undefined;
347348
readonly smsRegionConfig?: SmsRegionConfig;
348349
toJSON(): object;
349350
}
@@ -362,6 +363,35 @@ export interface ProviderIdentifier {
362363
providerUid: string;
363364
}
364365

366+
// @public
367+
export type RecaptchaAction = 'BLOCK';
368+
369+
// @public
370+
export interface RecaptchaConfig {
371+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
372+
managedRules?: RecaptchaManagedRule[];
373+
recaptchaKeys?: RecaptchaKey[];
374+
useAccountDefender?: boolean;
375+
}
376+
377+
// @public
378+
export interface RecaptchaKey {
379+
key: string;
380+
type?: RecaptchaKeyClientType;
381+
}
382+
383+
// @public
384+
export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID';
385+
386+
// @public
387+
export interface RecaptchaManagedRule {
388+
action?: RecaptchaAction;
389+
endScore: number;
390+
}
391+
392+
// @public
393+
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
394+
365395
// @public
366396
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
367397
callbackURL?: string;
@@ -397,6 +427,7 @@ export class Tenant {
397427
readonly displayName?: string;
398428
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
399429
get multiFactorConfig(): MultiFactorConfig | undefined;
430+
get recaptchaConfig(): RecaptchaConfig | undefined;
400431
readonly smsRegionConfig?: SmsRegionConfig;
401432
readonly tenantId: string;
402433
readonly testPhoneNumbers?: {
@@ -448,6 +479,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
448479
// @public
449480
export interface UpdateProjectConfigRequest {
450481
multiFactorConfig?: MultiFactorConfig;
482+
recaptchaConfig?: RecaptchaConfig;
451483
smsRegionConfig?: SmsRegionConfig;
452484
}
453485

@@ -471,6 +503,7 @@ export interface UpdateTenantRequest {
471503
displayName?: string;
472504
emailSignInConfig?: EmailSignInProviderConfig;
473505
multiFactorConfig?: MultiFactorConfig;
506+
recaptchaConfig?: RecaptchaConfig;
474507
smsRegionConfig?: SmsRegionConfig;
475508
testPhoneNumbers?: {
476509
[phoneNumber: string]: string;

‎src/auth/auth-config.ts

+224
Original file line numberDiff line numberDiff line change
@@ -1722,3 +1722,227 @@ export class SmsRegionsAuthConfig {
17221722
}
17231723
}
17241724
}
1725+
/**
1726+
* Enforcement state of reCAPTCHA protection.
1727+
* - 'OFF': Unenforced.
1728+
* - 'AUDIT': Create assessment but don't enforce the result.
1729+
* - 'ENFORCE': Create assessment and enforce the result.
1730+
*/
1731+
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
1732+
1733+
/**
1734+
* The actions to take for reCAPTCHA-protected requests.
1735+
* - 'BLOCK': The reCAPTCHA-protected request will be blocked.
1736+
*/
1737+
export type RecaptchaAction = 'BLOCK';
1738+
1739+
/**
1740+
* The config for a reCAPTCHA action rule.
1741+
*/
1742+
export interface RecaptchaManagedRule {
1743+
/**
1744+
* The action will be enforced if the reCAPTCHA score of a request is larger than endScore.
1745+
*/
1746+
endScore: number;
1747+
/**
1748+
* The action for reCAPTCHA-protected requests.
1749+
*/
1750+
action?: RecaptchaAction;
1751+
}
1752+
1753+
/**
1754+
* The key's platform type.
1755+
*/
1756+
export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID';
1757+
1758+
/**
1759+
* The reCAPTCHA key config.
1760+
*/
1761+
export interface RecaptchaKey {
1762+
/**
1763+
* The key's client platform type.
1764+
*/
1765+
type?: RecaptchaKeyClientType;
1766+
1767+
/**
1768+
* The reCAPTCHA site key.
1769+
*/
1770+
key: string;
1771+
}
1772+
1773+
/**
1774+
* The request interface for updating a reCAPTCHA Config.
1775+
* By enabling reCAPTCHA Enterprise Integration you are
1776+
* agreeing to reCAPTCHA Enterprise
1777+
* {@link https://cloud.google.com/terms/service-terms | Term of Service}.
1778+
*/
1779+
export interface RecaptchaConfig {
1780+
/**
1781+
* The enforcement state of the email password provider.
1782+
*/
1783+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1784+
/**
1785+
* The reCAPTCHA managed rules.
1786+
*/
1787+
managedRules?: RecaptchaManagedRule[];
1788+
1789+
/**
1790+
* The reCAPTCHA keys.
1791+
*/
1792+
recaptchaKeys?: RecaptchaKey[];
1793+
1794+
/**
1795+
* Whether to use account defender for reCAPTCHA assessment.
1796+
* The default value is false.
1797+
*/
1798+
useAccountDefender?: boolean;
1799+
}
1800+
1801+
export class RecaptchaAuthConfig implements RecaptchaConfig {
1802+
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1803+
public readonly managedRules?: RecaptchaManagedRule[];
1804+
public readonly recaptchaKeys?: RecaptchaKey[];
1805+
public readonly useAccountDefender?: boolean;
1806+
1807+
constructor(recaptchaConfig: RecaptchaConfig) {
1808+
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
1809+
this.managedRules = recaptchaConfig.managedRules;
1810+
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
1811+
this.useAccountDefender = recaptchaConfig.useAccountDefender;
1812+
}
1813+
1814+
/**
1815+
* Validates the RecaptchaConfig options object. Throws an error on failure.
1816+
* @param options - The options object to validate.
1817+
*/
1818+
public static validate(options: RecaptchaConfig): void {
1819+
const validKeys = {
1820+
emailPasswordEnforcementState: true,
1821+
managedRules: true,
1822+
recaptchaKeys: true,
1823+
useAccountDefender: true,
1824+
};
1825+
1826+
if (!validator.isNonNullObject(options)) {
1827+
throw new FirebaseAuthError(
1828+
AuthClientErrorCode.INVALID_CONFIG,
1829+
'"RecaptchaConfig" must be a non-null object.',
1830+
);
1831+
}
1832+
1833+
for (const key in options) {
1834+
if (!(key in validKeys)) {
1835+
throw new FirebaseAuthError(
1836+
AuthClientErrorCode.INVALID_CONFIG,
1837+
`"${key}" is not a valid RecaptchaConfig parameter.`,
1838+
);
1839+
}
1840+
}
1841+
1842+
// Validation
1843+
if (typeof options.emailPasswordEnforcementState !== undefined) {
1844+
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
1845+
throw new FirebaseAuthError(
1846+
AuthClientErrorCode.INVALID_ARGUMENT,
1847+
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
1848+
);
1849+
}
1850+
1851+
if (options.emailPasswordEnforcementState !== 'OFF' &&
1852+
options.emailPasswordEnforcementState !== 'AUDIT' &&
1853+
options.emailPasswordEnforcementState !== 'ENFORCE') {
1854+
throw new FirebaseAuthError(
1855+
AuthClientErrorCode.INVALID_CONFIG,
1856+
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
1857+
);
1858+
}
1859+
}
1860+
1861+
if (typeof options.managedRules !== 'undefined') {
1862+
// Validate array
1863+
if (!validator.isArray(options.managedRules)) {
1864+
throw new FirebaseAuthError(
1865+
AuthClientErrorCode.INVALID_CONFIG,
1866+
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
1867+
);
1868+
}
1869+
// Validate each rule of the array
1870+
options.managedRules.forEach((managedRule) => {
1871+
RecaptchaAuthConfig.validateManagedRule(managedRule);
1872+
});
1873+
}
1874+
1875+
if (typeof options.useAccountDefender != 'undefined') {
1876+
if (!validator.isBoolean(options.useAccountDefender)) {
1877+
throw new FirebaseAuthError(
1878+
AuthClientErrorCode.INVALID_CONFIG,
1879+
'"RecaptchaConfig.useAccountDefender" must be a boolean value".',
1880+
);
1881+
}
1882+
}
1883+
}
1884+
1885+
/**
1886+
* Validate each element in ManagedRule array
1887+
* @param options - The options object to validate.
1888+
*/
1889+
private static validateManagedRule(options: RecaptchaManagedRule): void {
1890+
const validKeys = {
1891+
endScore: true,
1892+
action: true,
1893+
}
1894+
if (!validator.isNonNullObject(options)) {
1895+
throw new FirebaseAuthError(
1896+
AuthClientErrorCode.INVALID_CONFIG,
1897+
'"RecaptchaManagedRule" must be a non-null object.',
1898+
);
1899+
}
1900+
// Check for unsupported top level attributes.
1901+
for (const key in options) {
1902+
if (!(key in validKeys)) {
1903+
throw new FirebaseAuthError(
1904+
AuthClientErrorCode.INVALID_CONFIG,
1905+
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
1906+
);
1907+
}
1908+
}
1909+
1910+
// Validate content.
1911+
if (typeof options.action !== 'undefined' &&
1912+
options.action !== 'BLOCK') {
1913+
throw new FirebaseAuthError(
1914+
AuthClientErrorCode.INVALID_CONFIG,
1915+
'"RecaptchaManagedRule.action" must be "BLOCK".',
1916+
);
1917+
}
1918+
}
1919+
1920+
/**
1921+
* Returns a JSON-serializable representation of this object.
1922+
* @returns The JSON-serializable object representation of the ReCaptcha config instance
1923+
*/
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;
1934+
}
1935+
if (typeof json.managedRules === 'undefined') {
1936+
delete json.managedRules;
1937+
}
1938+
if (typeof json.recaptchaKeys === 'undefined') {
1939+
delete json.recaptchaKeys;
1940+
}
1941+
1942+
if (typeof json.useAccountDefender === 'undefined') {
1943+
delete json.useAccountDefender;
1944+
}
1945+
1946+
return json;
1947+
}
1948+
}

‎src/auth/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export {
8484
OAuthResponseType,
8585
OIDCAuthProviderConfig,
8686
OIDCUpdateAuthProviderRequest,
87+
RecaptchaAction,
88+
RecaptchaConfig,
89+
RecaptchaKey,
90+
RecaptchaKeyClientType,
91+
RecaptchaManagedRule,
92+
RecaptchaProviderEnforcementState,
8793
SAMLAuthProviderConfig,
8894
SAMLUpdateAuthProviderRequest,
8995
SmsRegionConfig,

‎src/auth/project-config-manager.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,10 @@ import {
2020
} from './auth-api-request';
2121

2222
/**
23-
* Defines the project config manager used to help manage project config related operations.
24-
* This includes:
25-
* <ul>
26-
* <li>The ability to update and get project config.</li>
23+
* Manages (gets and updates) the current project config.
2724
*/
2825
export class ProjectConfigManager {
2926
private readonly authRequestHandler: AuthRequestHandler;
30-
3127
/**
3228
* Initializes a ProjectConfigManager instance for a specified FirebaseApp.
3329
*

0 commit comments

Comments
 (0)