Skip to content

Commit 7e90c9a

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 be100b7 commit 7e90c9a

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
@@ -354,6 +354,34 @@ export interface ProviderIdentifier {
354354
providerUid: string;
355355
}
356356

357+
// @public
358+
export type RecaptchaAction = 'BLOCK';
359+
360+
// @public
361+
export interface RecaptchaConfig {
362+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
363+
managedRules?: RecaptchaManagedRule[];
364+
recaptchaKeys?: RecaptchaKey[];
365+
}
366+
367+
// @public
368+
export interface RecaptchaKey {
369+
key: string;
370+
type?: RecaptchaKeyClientType;
371+
}
372+
373+
// @public
374+
export type RecaptchaKeyClientType = 'WEB';
375+
376+
// @public
377+
export interface RecaptchaManagedRule {
378+
action?: RecaptchaAction;
379+
endScore: number;
380+
}
381+
382+
// @public
383+
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
384+
357385
// @public
358386
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
359387
callbackURL?: string;
@@ -390,6 +418,7 @@ export class Tenant {
390418
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
391419
get multiFactorConfig(): MultiFactorConfig | undefined;
392420
readonly smsRegionConfig?: SmsRegionConfig;
421+
get recaptchaConfig(): RecaptchaConfig | undefined;
393422
readonly tenantId: string;
394423
readonly testPhoneNumbers?: {
395424
[phoneNumber: string]: string;
@@ -458,6 +487,7 @@ export interface UpdateTenantRequest {
458487
emailSignInConfig?: EmailSignInProviderConfig;
459488
multiFactorConfig?: MultiFactorConfig;
460489
smsRegionConfig?: SmsRegionConfig;
490+
recaptchaConfig?: RecaptchaConfig;
461491
testPhoneNumbers?: {
462492
[phoneNumber: string]: string;
463493
} | null;

src/auth/auth-config.ts

Lines changed: 145 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,22 +1640,156 @@ export interface RecaptchaKey {
16401640
/**
16411641
* The reCAPTCHA site key.
16421642
*/
1643-
key: string;
1643+
key: string;
16441644
}
16451645

1646+
/**
1647+
* The request interface for updating a reCAPTCHA Config.
1648+
*/
16461649
export interface RecaptchaConfig {
1647-
/**
1650+
/**
16481651
* The enforcement state of email password provider.
16491652
*/
1650-
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1653+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1654+
/**
1655+
* The reCAPTCHA managed rules.
1656+
*/
1657+
managedRules?: RecaptchaManagedRule[];
16511658

1652-
/**
1653-
* The reCAPTCHA managed rules.
1654-
*/
1655-
managedRules: RecaptchaManagedRule[];
1659+
/**
1660+
* The reCAPTCHA keys.
1661+
*/
1662+
recaptchaKeys?: RecaptchaKey[];
1663+
}
16561664

1657-
/**
1658-
* The reCAPTCHA keys.
1659-
*/
1660-
recaptchaKeys?: RecaptchaKey[];
1665+
export class RecaptchaAuthConfig implements RecaptchaConfig {
1666+
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1667+
public readonly managedRules?: RecaptchaManagedRule[];
1668+
public readonly recaptchaKeys?: RecaptchaKey[];
1669+
1670+
constructor(recaptchaConfig: RecaptchaConfig) {
1671+
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
1672+
this.managedRules = recaptchaConfig.managedRules;
1673+
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
1674+
}
1675+
1676+
/**
1677+
* Validates the RecaptchaConfig options object. Throws an error on failure.
1678+
* @param options - The options object to validate.
1679+
*/
1680+
public static validate(options: RecaptchaConfig): void {
1681+
const validKeys = {
1682+
emailPasswordEnforcementState: true,
1683+
managedRules: true,
1684+
recaptchaKeys: true,
1685+
};
1686+
1687+
if (!validator.isNonNullObject(options)) {
1688+
throw new FirebaseAuthError(
1689+
AuthClientErrorCode.INVALID_CONFIG,
1690+
'"RecaptchaConfig" must be a non-null object.',
1691+
);
1692+
}
1693+
1694+
for (const key in options) {
1695+
if (!(key in validKeys)) {
1696+
throw new FirebaseAuthError(
1697+
AuthClientErrorCode.INVALID_CONFIG,
1698+
`"${key}" is not a valid RecaptchaConfig parameter.`,
1699+
);
1700+
}
1701+
}
1702+
1703+
// Validation
1704+
if (typeof options.emailPasswordEnforcementState !== undefined) {
1705+
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
1706+
throw new FirebaseAuthError(
1707+
AuthClientErrorCode.INVALID_ARGUMENT,
1708+
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
1709+
);
1710+
}
1711+
1712+
if (options.emailPasswordEnforcementState !== 'OFF' &&
1713+
options.emailPasswordEnforcementState !== 'AUDIT' &&
1714+
options.emailPasswordEnforcementState !== 'ENFORCE') {
1715+
throw new FirebaseAuthError(
1716+
AuthClientErrorCode.INVALID_CONFIG,
1717+
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
1718+
);
1719+
}
1720+
}
1721+
1722+
if (typeof options.managedRules !== 'undefined') {
1723+
// Validate array
1724+
if (!validator.isArray(options.managedRules)) {
1725+
throw new FirebaseAuthError(
1726+
AuthClientErrorCode.INVALID_CONFIG,
1727+
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
1728+
);
1729+
}
1730+
// Validate each rule of the array
1731+
options.managedRules.forEach((managedRule) => {
1732+
RecaptchaAuthConfig.validateManagedRule(managedRule);
1733+
});
1734+
}
1735+
}
1736+
1737+
/**
1738+
* Validate each element in ManagedRule array
1739+
* @param options - The options object to validate.
1740+
*/
1741+
private static validateManagedRule(options: RecaptchaManagedRule): void {
1742+
const validKeys = {
1743+
endScore: true,
1744+
action: true,
1745+
}
1746+
if (!validator.isNonNullObject(options)) {
1747+
throw new FirebaseAuthError(
1748+
AuthClientErrorCode.INVALID_CONFIG,
1749+
'"RecaptchaManagedRule" must be a non-null object.',
1750+
);
1751+
}
1752+
// Check for unsupported top level attributes.
1753+
for (const key in options) {
1754+
if (!(key in validKeys)) {
1755+
throw new FirebaseAuthError(
1756+
AuthClientErrorCode.INVALID_CONFIG,
1757+
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
1758+
);
1759+
}
1760+
}
1761+
1762+
// Validate content.
1763+
if (typeof options.action !== 'undefined' &&
1764+
options.action !== 'BLOCK') {
1765+
throw new FirebaseAuthError(
1766+
AuthClientErrorCode.INVALID_CONFIG,
1767+
'"RecaptchaManagedRule.action" must be "BLOCK".',
1768+
);
1769+
}
1770+
}
1771+
1772+
/**
1773+
* Returns a JSON-serializable representation of this object.
1774+
* @returns The JSON-serializable object representation of the ReCaptcha config instance
1775+
*/
1776+
public toJSON(): object {
1777+
const json: any = {
1778+
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
1779+
managedRules: deepCopy(this.managedRules),
1780+
recaptchaKeys: deepCopy(this.recaptchaKeys)
1781+
}
1782+
1783+
if (typeof json.emailPasswordEnforcementState === 'undefined') {
1784+
delete json.emailPasswordEnforcementState;
1785+
}
1786+
if (typeof json.managedRules === 'undefined') {
1787+
delete json.managedRules;
1788+
}
1789+
if (typeof json.recaptchaKeys === 'undefined') {
1790+
delete json.recaptchaKeys;
1791+
}
1792+
1793+
return json;
1794+
}
16611795
}

src/auth/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export {
8383
OAuthResponseType,
8484
OIDCAuthProviderConfig,
8585
OIDCUpdateAuthProviderRequest,
86+
RecaptchaAction,
87+
RecaptchaConfig,
88+
RecaptchaKey,
89+
RecaptchaKeyClientType,
90+
RecaptchaManagedRule,
91+
RecaptchaProviderEnforcementState,
8692
SAMLAuthProviderConfig,
8793
SAMLUpdateAuthProviderRequest,
8894
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)