Skip to content

Commit 0d1a56f

Browse files
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 b300093 commit 0d1a56f

File tree

5 files changed

+406
-16
lines changed

5 files changed

+406
-16
lines changed

etc/firebase-admin.auth.api.md

+30
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,34 @@ export interface ProviderIdentifier {
264264
providerUid: string;
265265
}
266266

267+
// @public
268+
export type RecaptchaAction = 'BLOCK';
269+
270+
// @public
271+
export interface RecaptchaConfig {
272+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
273+
managedRules?: RecaptchaManagedRule[];
274+
recaptchaKeys?: RecaptchaKey[];
275+
}
276+
277+
// @public
278+
export interface RecaptchaKey {
279+
key: string;
280+
type?: RecaptchaKeyClientType;
281+
}
282+
283+
// @public
284+
export type RecaptchaKeyClientType = 'WEB';
285+
286+
// @public
287+
export interface RecaptchaManagedRule {
288+
action?: RecaptchaAction;
289+
endScore: number;
290+
}
291+
292+
// @public
293+
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';
294+
267295
// @public
268296
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
269297
callbackURL?: string;
@@ -296,6 +324,7 @@ export class Tenant {
296324
readonly displayName?: string;
297325
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
298326
get multiFactorConfig(): MultiFactorConfig | undefined;
327+
get recaptchaConfig(): RecaptchaConfig | undefined;
299328
readonly tenantId: string;
300329
readonly testPhoneNumbers?: {
301330
[phoneNumber: string]: string;
@@ -358,6 +387,7 @@ export interface UpdateTenantRequest {
358387
displayName?: string;
359388
emailSignInConfig?: EmailSignInProviderConfig;
360389
multiFactorConfig?: MultiFactorConfig;
390+
recaptchaConfig?: RecaptchaConfig;
361391
testPhoneNumbers?: {
362392
[phoneNumber: string]: string;
363393
} | null;

src/auth/auth-config.ts

+145-11
Original file line numberDiff line numberDiff line change
@@ -1497,22 +1497,156 @@ export interface RecaptchaKey {
14971497
/**
14981498
* The reCAPTCHA site key.
14991499
*/
1500-
key: string;
1500+
key: string;
15011501
}
15021502

1503+
/**
1504+
* The request interface for updating a reCAPTCHA Config.
1505+
*/
15031506
export interface RecaptchaConfig {
1504-
/**
1507+
/**
15051508
* The enforcement state of email password provider.
15061509
*/
1507-
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1510+
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1511+
/**
1512+
* The reCAPTCHA managed rules.
1513+
*/
1514+
managedRules?: RecaptchaManagedRule[];
15081515

1509-
/**
1510-
* The reCAPTCHA managed rules.
1511-
*/
1512-
managedRules: RecaptchaManagedRule[];
1516+
/**
1517+
* The reCAPTCHA keys.
1518+
*/
1519+
recaptchaKeys?: RecaptchaKey[];
1520+
}
15131521

1514-
/**
1515-
* The reCAPTCHA keys.
1516-
*/
1517-
recaptchaKeys?: RecaptchaKey[];
1522+
export class RecaptchaAuthConfig implements RecaptchaConfig {
1523+
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
1524+
public readonly managedRules?: RecaptchaManagedRule[];
1525+
public readonly recaptchaKeys?: RecaptchaKey[];
1526+
1527+
constructor(recaptchaConfig: RecaptchaConfig) {
1528+
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
1529+
this.managedRules = recaptchaConfig.managedRules;
1530+
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
1531+
}
1532+
1533+
/**
1534+
* Validates the RecaptchaConfig options object. Throws an error on failure.
1535+
* @param options - The options object to validate.
1536+
*/
1537+
public static validate(options: RecaptchaConfig): void {
1538+
const validKeys = {
1539+
emailPasswordEnforcementState: true,
1540+
managedRules: true,
1541+
recaptchaKeys: true,
1542+
};
1543+
1544+
if (!validator.isNonNullObject(options)) {
1545+
throw new FirebaseAuthError(
1546+
AuthClientErrorCode.INVALID_CONFIG,
1547+
'"RecaptchaConfig" must be a non-null object.',
1548+
);
1549+
}
1550+
1551+
for (const key in options) {
1552+
if (!(key in validKeys)) {
1553+
throw new FirebaseAuthError(
1554+
AuthClientErrorCode.INVALID_CONFIG,
1555+
`"${key}" is not a valid RecaptchaConfig parameter.`,
1556+
);
1557+
}
1558+
}
1559+
1560+
// Validation
1561+
if (typeof options.emailPasswordEnforcementState !== undefined) {
1562+
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
1563+
throw new FirebaseAuthError(
1564+
AuthClientErrorCode.INVALID_ARGUMENT,
1565+
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
1566+
);
1567+
}
1568+
1569+
if (options.emailPasswordEnforcementState !== 'OFF' &&
1570+
options.emailPasswordEnforcementState !== 'AUDIT' &&
1571+
options.emailPasswordEnforcementState !== 'ENFORCE') {
1572+
throw new FirebaseAuthError(
1573+
AuthClientErrorCode.INVALID_CONFIG,
1574+
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
1575+
);
1576+
}
1577+
}
1578+
1579+
if (typeof options.managedRules !== 'undefined') {
1580+
// Validate array
1581+
if (!validator.isArray(options.managedRules)) {
1582+
throw new FirebaseAuthError(
1583+
AuthClientErrorCode.INVALID_CONFIG,
1584+
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
1585+
);
1586+
}
1587+
// Validate each rule of the array
1588+
options.managedRules.forEach((managedRule) => {
1589+
RecaptchaAuthConfig.validateManagedRule(managedRule);
1590+
});
1591+
}
1592+
}
1593+
1594+
/**
1595+
* Validate each element in ManagedRule array
1596+
* @param options - The options object to validate.
1597+
*/
1598+
private static validateManagedRule(options: RecaptchaManagedRule): void {
1599+
const validKeys = {
1600+
endScore: true,
1601+
action: true,
1602+
}
1603+
if (!validator.isNonNullObject(options)) {
1604+
throw new FirebaseAuthError(
1605+
AuthClientErrorCode.INVALID_CONFIG,
1606+
'"RecaptchaManagedRule" must be a non-null object.',
1607+
);
1608+
}
1609+
// Check for unsupported top level attributes.
1610+
for (const key in options) {
1611+
if (!(key in validKeys)) {
1612+
throw new FirebaseAuthError(
1613+
AuthClientErrorCode.INVALID_CONFIG,
1614+
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
1615+
);
1616+
}
1617+
}
1618+
1619+
// Validate content.
1620+
if (typeof options.action !== 'undefined' &&
1621+
options.action !== 'BLOCK') {
1622+
throw new FirebaseAuthError(
1623+
AuthClientErrorCode.INVALID_CONFIG,
1624+
'"RecaptchaManagedRule.action" must be "BLOCK".',
1625+
);
1626+
}
1627+
}
1628+
1629+
/**
1630+
* Returns a JSON-serializable representation of this object.
1631+
* @returns The JSON-serializable object representation of the ReCaptcha config instance
1632+
*/
1633+
public toJSON(): object {
1634+
const json: any = {
1635+
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
1636+
managedRules: deepCopy(this.managedRules),
1637+
recaptchaKeys: deepCopy(this.recaptchaKeys)
1638+
}
1639+
1640+
if (typeof json.emailPasswordEnforcementState === 'undefined') {
1641+
delete json.emailPasswordEnforcementState;
1642+
}
1643+
if (typeof json.managedRules === 'undefined') {
1644+
delete json.managedRules;
1645+
}
1646+
if (typeof json.recaptchaKeys === 'undefined') {
1647+
delete json.recaptchaKeys;
1648+
}
1649+
1650+
return json;
1651+
}
15181652
}

src/auth/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export {
7979
OAuthResponseType,
8080
OIDCAuthProviderConfig,
8181
OIDCUpdateAuthProviderRequest,
82+
RecaptchaAction,
83+
RecaptchaConfig,
84+
RecaptchaKey,
85+
RecaptchaKeyClientType,
86+
RecaptchaManagedRule,
87+
RecaptchaProviderEnforcementState,
8288
SAMLAuthProviderConfig,
8389
SAMLUpdateAuthProviderRequest,
8490
UserProvider,

src/auth/tenant.ts

+34-1
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,
24+
MultiFactorAuthConfig, RecaptchaAuthConfig, RecaptchaConfig
2525
} from './auth-config';
2626

2727
/**
@@ -54,6 +54,11 @@ export interface UpdateTenantRequest {
5454
* Passing null clears the previously save phone number / code pairs.
5555
*/
5656
testPhoneNumbers?: { [phoneNumber: string]: string } | null;
57+
58+
/**
59+
* The recaptcha configuration to update on the tenant.
60+
*/
61+
recaptchaConfig?: RecaptchaConfig;
5762
}
5863

5964
/**
@@ -68,6 +73,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque
6873
enableAnonymousUser?: boolean;
6974
mfaConfig?: MultiFactorAuthServerConfig;
7075
testPhoneNumbers?: {[key: string]: string};
76+
recaptchaConfig?: RecaptchaConfig;
7177
}
7278

7379
/** The tenant server response interface. */
@@ -79,6 +85,7 @@ export interface TenantServerResponse {
7985
enableAnonymousUser?: boolean;
8086
mfaConfig?: MultiFactorAuthServerConfig;
8187
testPhoneNumbers?: {[key: string]: string};
88+
recaptchaConfig? : RecaptchaConfig;
8289
}
8390

8491
/**
@@ -123,6 +130,10 @@ export class Tenant {
123130
private readonly emailSignInConfig_?: EmailSignInConfig;
124131
private readonly multiFactorConfig_?: MultiFactorAuthConfig;
125132

133+
/*
134+
* The map conatining the reCAPTCHA config.
135+
*/
136+
private readonly recaptchaConfig_?: RecaptchaAuthConfig;
126137
/**
127138
* Builds the corresponding server request for a TenantOptions object.
128139
*
@@ -152,6 +163,9 @@ export class Tenant {
152163
// null will clear existing test phone numbers. Translate to empty object.
153164
request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {};
154165
}
166+
if (typeof tenantOptions.recaptchaConfig !== 'undefined') {
167+
request.recaptchaConfig = tenantOptions.recaptchaConfig;
168+
}
155169
return request;
156170
}
157171

@@ -185,6 +199,7 @@ export class Tenant {
185199
anonymousSignInEnabled: true,
186200
multiFactorConfig: true,
187201
testPhoneNumbers: true,
202+
recaptchaConfig: true,
188203
};
189204
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
190205
if (!validator.isNonNullObject(request)) {
@@ -231,6 +246,10 @@ export class Tenant {
231246
// This will throw an error if invalid.
232247
MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig);
233248
}
249+
// Validate reCAPTCHAConfig type if provided.
250+
if (typeof request.recaptchaConfig !== 'undefined') {
251+
RecaptchaAuthConfig.validate(request.recaptchaConfig);
252+
}
234253
}
235254

236255
/**
@@ -265,6 +284,9 @@ export class Tenant {
265284
if (typeof response.testPhoneNumbers !== 'undefined') {
266285
this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {});
267286
}
287+
if (typeof response.recaptchaConfig !== 'undefined') {
288+
this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
289+
}
268290
}
269291

270292
/**
@@ -281,6 +303,13 @@ export class Tenant {
281303
return this.multiFactorConfig_;
282304
}
283305

306+
/**
307+
* The recaptcha config auth configuration of the current tenant.
308+
*/
309+
get recaptchaConfig(): RecaptchaConfig | undefined {
310+
return this.recaptchaConfig_;
311+
}
312+
284313
/**
285314
* Returns a JSON-serializable representation of this object.
286315
*
@@ -294,13 +323,17 @@ export class Tenant {
294323
multiFactorConfig: this.multiFactorConfig_?.toJSON(),
295324
anonymousSignInEnabled: this.anonymousSignInEnabled,
296325
testPhoneNumbers: this.testPhoneNumbers,
326+
recaptchaConfig: this.recaptchaConfig_?.toJSON(),
297327
};
298328
if (typeof json.multiFactorConfig === 'undefined') {
299329
delete json.multiFactorConfig;
300330
}
301331
if (typeof json.testPhoneNumbers === 'undefined') {
302332
delete json.testPhoneNumbers;
303333
}
334+
if (typeof json.recaptchaConfig === 'undefined') {
335+
delete json.recaptchaConfig;
336+
}
304337
return json;
305338
}
306339
}

0 commit comments

Comments
 (0)