Skip to content

Commit ad904c4

Browse files
committed
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.
1 parent b10d9d6 commit ad904c4

File tree

5 files changed

+406
-16
lines changed

5 files changed

+406
-16
lines changed

etc/firebase-admin.auth.api.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,34 @@ export interface ProviderIdentifier {
362362
providerUid: string;
363363
}
364364

365+
// @public
366+
export type RecaptchaAction = 'BLOCK';
367+
368+
// @public
369+
export interface RecaptchaConfig {
370+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
371+
managedRules?: RecaptchaManagedRule[];
372+
recaptchaKeys?: RecaptchaKey[];
373+
}
374+
375+
// @public
376+
export interface RecaptchaKey {
377+
key: string;
378+
type?: RecaptchaKeyClientType;
379+
}
380+
381+
// @public
382+
export type RecaptchaKeyClientType = 'WEB';
383+
384+
// @public
385+
export interface RecaptchaManagedRule {
386+
action?: RecaptchaAction;
387+
endScore: number;
388+
}
389+
390+
// @public
391+
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
392+
365393
// @public
366394
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
367395
callbackURL?: string;
@@ -398,6 +426,7 @@ export class Tenant {
398426
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
399427
get multiFactorConfig(): MultiFactorConfig | undefined;
400428
readonly smsRegionConfig?: SmsRegionConfig;
429+
get recaptchaConfig(): RecaptchaConfig | undefined;
401430
readonly tenantId: string;
402431
readonly testPhoneNumbers?: {
403432
[phoneNumber: string]: string;
@@ -472,6 +501,7 @@ export interface UpdateTenantRequest {
472501
emailSignInConfig?: EmailSignInProviderConfig;
473502
multiFactorConfig?: MultiFactorConfig;
474503
smsRegionConfig?: SmsRegionConfig;
504+
recaptchaConfig?: RecaptchaConfig;
475505
testPhoneNumbers?: {
476506
[phoneNumber: string]: string;
477507
} | null;

src/auth/auth-config.ts

Lines changed: 145 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,22 +1768,156 @@ export interface RecaptchaKey {
17681768
/**
17691769
* The reCAPTCHA site key.
17701770
*/
1771-
key: string;
1771+
key: string;
17721772
}
17731773

1774+
/**
1775+
* The request interface for updating a reCAPTCHA Config.
1776+
*/
17741777
export interface RecaptchaConfig {
1775-
/**
1778+
/**
17761779
* The enforcement state of email password provider.
17771780
*/
1778-
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1781+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1782+
/**
1783+
* The reCAPTCHA managed rules.
1784+
*/
1785+
managedRules?: RecaptchaManagedRule[];
17791786

1780-
/**
1781-
* The reCAPTCHA managed rules.
1782-
*/
1783-
managedRules: RecaptchaManagedRule[];
1787+
/**
1788+
* The reCAPTCHA keys.
1789+
*/
1790+
recaptchaKeys?: RecaptchaKey[];
1791+
}
17841792

1785-
/**
1786-
* The reCAPTCHA keys.
1787-
*/
1788-
recaptchaKeys?: RecaptchaKey[];
1793+
export class RecaptchaAuthConfig implements RecaptchaConfig {
1794+
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1795+
public readonly managedRules?: RecaptchaManagedRule[];
1796+
public readonly recaptchaKeys?: RecaptchaKey[];
1797+
1798+
constructor(recaptchaConfig: RecaptchaConfig) {
1799+
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
1800+
this.managedRules = recaptchaConfig.managedRules;
1801+
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
1802+
}
1803+
1804+
/**
1805+
* Validates the RecaptchaConfig options object. Throws an error on failure.
1806+
* @param options - The options object to validate.
1807+
*/
1808+
public static validate(options: RecaptchaConfig): void {
1809+
const validKeys = {
1810+
emailPasswordEnforcementState: true,
1811+
managedRules: true,
1812+
recaptchaKeys: true,
1813+
};
1814+
1815+
if (!validator.isNonNullObject(options)) {
1816+
throw new FirebaseAuthError(
1817+
AuthClientErrorCode.INVALID_CONFIG,
1818+
'"RecaptchaConfig" must be a non-null object.',
1819+
);
1820+
}
1821+
1822+
for (const key in options) {
1823+
if (!(key in validKeys)) {
1824+
throw new FirebaseAuthError(
1825+
AuthClientErrorCode.INVALID_CONFIG,
1826+
`"${key}" is not a valid RecaptchaConfig parameter.`,
1827+
);
1828+
}
1829+
}
1830+
1831+
// Validation
1832+
if (typeof options.emailPasswordEnforcementState !== undefined) {
1833+
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
1834+
throw new FirebaseAuthError(
1835+
AuthClientErrorCode.INVALID_ARGUMENT,
1836+
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
1837+
);
1838+
}
1839+
1840+
if (options.emailPasswordEnforcementState !== 'OFF' &&
1841+
options.emailPasswordEnforcementState !== 'AUDIT' &&
1842+
options.emailPasswordEnforcementState !== 'ENFORCE') {
1843+
throw new FirebaseAuthError(
1844+
AuthClientErrorCode.INVALID_CONFIG,
1845+
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
1846+
);
1847+
}
1848+
}
1849+
1850+
if (typeof options.managedRules !== 'undefined') {
1851+
// Validate array
1852+
if (!validator.isArray(options.managedRules)) {
1853+
throw new FirebaseAuthError(
1854+
AuthClientErrorCode.INVALID_CONFIG,
1855+
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
1856+
);
1857+
}
1858+
// Validate each rule of the array
1859+
options.managedRules.forEach((managedRule) => {
1860+
RecaptchaAuthConfig.validateManagedRule(managedRule);
1861+
});
1862+
}
1863+
}
1864+
1865+
/**
1866+
* Validate each element in ManagedRule array
1867+
* @param options - The options object to validate.
1868+
*/
1869+
private static validateManagedRule(options: RecaptchaManagedRule): void {
1870+
const validKeys = {
1871+
endScore: true,
1872+
action: true,
1873+
}
1874+
if (!validator.isNonNullObject(options)) {
1875+
throw new FirebaseAuthError(
1876+
AuthClientErrorCode.INVALID_CONFIG,
1877+
'"RecaptchaManagedRule" must be a non-null object.',
1878+
);
1879+
}
1880+
// Check for unsupported top level attributes.
1881+
for (const key in options) {
1882+
if (!(key in validKeys)) {
1883+
throw new FirebaseAuthError(
1884+
AuthClientErrorCode.INVALID_CONFIG,
1885+
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
1886+
);
1887+
}
1888+
}
1889+
1890+
// Validate content.
1891+
if (typeof options.action !== 'undefined' &&
1892+
options.action !== 'BLOCK') {
1893+
throw new FirebaseAuthError(
1894+
AuthClientErrorCode.INVALID_CONFIG,
1895+
'"RecaptchaManagedRule.action" must be "BLOCK".',
1896+
);
1897+
}
1898+
}
1899+
1900+
/**
1901+
* Returns a JSON-serializable representation of this object.
1902+
* @returns The JSON-serializable object representation of the ReCaptcha config instance
1903+
*/
1904+
public toJSON(): object {
1905+
const json: any = {
1906+
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
1907+
managedRules: deepCopy(this.managedRules),
1908+
recaptchaKeys: deepCopy(this.recaptchaKeys)
1909+
}
1910+
1911+
if (typeof json.emailPasswordEnforcementState === 'undefined') {
1912+
delete json.emailPasswordEnforcementState;
1913+
}
1914+
if (typeof json.managedRules === 'undefined') {
1915+
delete json.managedRules;
1916+
}
1917+
if (typeof json.recaptchaKeys === 'undefined') {
1918+
delete json.recaptchaKeys;
1919+
}
1920+
1921+
return json;
1922+
}
17891923
}

src/auth/index.ts

Lines changed: 6 additions & 0 deletions
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/tenant.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
2121
import {
2222
EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig,
2323
MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig,
24-
MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig
24+
MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig
2525
} from './auth-config';
2626

2727
/**
@@ -59,6 +59,11 @@ export interface UpdateTenantRequest {
5959
* The SMS configuration to update on the project.
6060
*/
6161
smsRegionConfig?: SmsRegionConfig;
62+
63+
/**
64+
* The recaptcha configuration to update on the tenant.
65+
*/
66+
recaptchaConfig?: RecaptchaConfig;
6267
}
6368

6469
/**
@@ -74,6 +79,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque
7479
mfaConfig?: MultiFactorAuthServerConfig;
7580
testPhoneNumbers?: {[key: string]: string};
7681
smsRegionConfig?: SmsRegionConfig;
82+
recaptchaConfig?: RecaptchaConfig;
7783
}
7884

7985
/** The tenant server response interface. */
@@ -86,6 +92,7 @@ export interface TenantServerResponse {
8692
mfaConfig?: MultiFactorAuthServerConfig;
8793
testPhoneNumbers?: {[key: string]: string};
8894
smsRegionConfig?: SmsRegionConfig;
95+
recaptchaConfig? : RecaptchaConfig;
8996
}
9097

9198
/**
@@ -130,6 +137,10 @@ export class Tenant {
130137
private readonly emailSignInConfig_?: EmailSignInConfig;
131138
private readonly multiFactorConfig_?: MultiFactorAuthConfig;
132139

140+
/*
141+
* The map conatining the reCAPTCHA config.
142+
*/
143+
private readonly recaptchaConfig_?: RecaptchaAuthConfig;
133144
/**
134145
* The SMS Regions Config to update a tenant.
135146
* Configures the regions where users are allowed to send verification SMS.
@@ -169,6 +180,9 @@ export class Tenant {
169180
if (typeof tenantOptions.smsRegionConfig !== 'undefined') {
170181
request.smsRegionConfig = tenantOptions.smsRegionConfig;
171182
}
183+
if (typeof tenantOptions.recaptchaConfig !== 'undefined') {
184+
request.recaptchaConfig = tenantOptions.recaptchaConfig;
185+
}
172186
return request;
173187
}
174188

@@ -203,6 +217,7 @@ export class Tenant {
203217
multiFactorConfig: true,
204218
testPhoneNumbers: true,
205219
smsRegionConfig: true,
220+
recaptchaConfig: true,
206221
};
207222
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
208223
if (!validator.isNonNullObject(request)) {
@@ -253,6 +268,10 @@ export class Tenant {
253268
if (typeof request.smsRegionConfig != 'undefined') {
254269
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
255270
}
271+
// Validate reCAPTCHAConfig type if provided.
272+
if (typeof request.recaptchaConfig !== 'undefined') {
273+
RecaptchaAuthConfig.validate(request.recaptchaConfig);
274+
}
256275
}
257276

258277
/**
@@ -290,6 +309,9 @@ export class Tenant {
290309
if (typeof response.smsRegionConfig !== 'undefined') {
291310
this.smsRegionConfig = deepCopy(response.smsRegionConfig);
292311
}
312+
if (typeof response.recaptchaConfig !== 'undefined') {
313+
this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
314+
}
293315
}
294316

295317
/**
@@ -306,6 +328,13 @@ export class Tenant {
306328
return this.multiFactorConfig_;
307329
}
308330

331+
/**
332+
* The recaptcha config auth configuration of the current tenant.
333+
*/
334+
get recaptchaConfig(): RecaptchaConfig | undefined {
335+
return this.recaptchaConfig_;
336+
}
337+
309338
/**
310339
* Returns a JSON-serializable representation of this object.
311340
*
@@ -320,6 +349,7 @@ export class Tenant {
320349
anonymousSignInEnabled: this.anonymousSignInEnabled,
321350
testPhoneNumbers: this.testPhoneNumbers,
322351
smsRegionConfig: deepCopy(this.smsRegionConfig),
352+
recaptchaConfig: this.recaptchaConfig_?.toJSON(),
323353
};
324354
if (typeof json.multiFactorConfig === 'undefined') {
325355
delete json.multiFactorConfig;
@@ -330,6 +360,9 @@ export class Tenant {
330360
if (typeof json.smsRegionConfig === 'undefined') {
331361
delete json.smsRegionConfig;
332362
}
363+
if (typeof json.recaptchaConfig === 'undefined') {
364+
delete json.recaptchaConfig;
365+
}
333366
return json;
334367
}
335368
}

0 commit comments

Comments
 (0)