Skip to content

Commit

Permalink
Create/Update tenant with ReCAPTCHA Config (#1586)
Browse files Browse the repository at this point in the history
* Support reCaptcha config /create update on tenants.
 - Support create and update tenants with reCaptcha config.
 - Added reCaptcha unit tests on tenants operations.
  • Loading branch information
Xiaoshouzi-gh committed Apr 11, 2023
1 parent 4d0c751 commit 9dea070
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 16 deletions.
30 changes: 30 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,34 @@ export interface ProviderIdentifier {
providerUid: string;
}

// @public
export type RecaptchaAction = 'BLOCK';

// @public
export interface RecaptchaConfig {
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
managedRules?: RecaptchaManagedRule[];
recaptchaKeys?: RecaptchaKey[];
}

// @public
export interface RecaptchaKey {
key: string;
type?: RecaptchaKeyClientType;
}

// @public
export type RecaptchaKeyClientType = 'WEB';

// @public
export interface RecaptchaManagedRule {
action?: RecaptchaAction;
endScore: number;
}

// @public
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';

// @public
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
callbackURL?: string;
Expand Down Expand Up @@ -398,6 +426,7 @@ export class Tenant {
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
get recaptchaConfig(): RecaptchaConfig | undefined;
readonly tenantId: string;
readonly testPhoneNumbers?: {
[phoneNumber: string]: string;
Expand Down Expand Up @@ -472,6 +501,7 @@ export interface UpdateTenantRequest {
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig?: RecaptchaConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
} | null;
Expand Down
156 changes: 145 additions & 11 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,22 +1768,156 @@ export interface RecaptchaKey {
/**
* The reCAPTCHA site key.
*/
key: string;
key: string;
}

/**
* The request interface for updating a reCAPTCHA Config.
*/
export interface RecaptchaConfig {
/**
/**
* The enforcement state of email password provider.
*/
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
/**
* The reCAPTCHA managed rules.
*/
managedRules?: RecaptchaManagedRule[];

/**
* The reCAPTCHA managed rules.
*/
managedRules: RecaptchaManagedRule[];
/**
* The reCAPTCHA keys.
*/
recaptchaKeys?: RecaptchaKey[];
}

/**
* The reCAPTCHA keys.
*/
recaptchaKeys?: RecaptchaKey[];
export class RecaptchaAuthConfig implements RecaptchaConfig {
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
public readonly managedRules?: RecaptchaManagedRule[];
public readonly recaptchaKeys?: RecaptchaKey[];

constructor(recaptchaConfig: RecaptchaConfig) {
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
this.managedRules = recaptchaConfig.managedRules;
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
}

/**
* Validates the RecaptchaConfig options object. Throws an error on failure.
* @param options - The options object to validate.
*/
public static validate(options: RecaptchaConfig): void {
const validKeys = {
emailPasswordEnforcementState: true,
managedRules: true,
recaptchaKeys: true,
};

if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig" must be a non-null object.',
);
}

for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid RecaptchaConfig parameter.`,
);
}
}

// Validation
if (typeof options.emailPasswordEnforcementState !== undefined) {
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',
);
}

if (options.emailPasswordEnforcementState !== 'OFF' &&
options.emailPasswordEnforcementState !== 'AUDIT' &&
options.emailPasswordEnforcementState !== 'ENFORCE') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
);
}
}

if (typeof options.managedRules !== 'undefined') {
// Validate array
if (!validator.isArray(options.managedRules)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
);
}
// Validate each rule of the array
options.managedRules.forEach((managedRule) => {
RecaptchaAuthConfig.validateManagedRule(managedRule);
});
}
}

/**
* Validate each element in ManagedRule array
* @param options - The options object to validate.
*/
private static validateManagedRule(options: RecaptchaManagedRule): void {
const validKeys = {
endScore: true,
action: true,
}
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaManagedRule" must be a non-null object.',
);
}
// Check for unsupported top level attributes.
for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
);
}
}

// Validate content.
if (typeof options.action !== 'undefined' &&
options.action !== 'BLOCK') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaManagedRule.action" must be "BLOCK".',
);
}
}

/**
* Returns a JSON-serializable representation of this object.
* @returns The JSON-serializable object representation of the ReCaptcha config instance
*/
public toJSON(): object {
const json: any = {
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
managedRules: deepCopy(this.managedRules),
recaptchaKeys: deepCopy(this.recaptchaKeys)
}

if (typeof json.emailPasswordEnforcementState === 'undefined') {
delete json.emailPasswordEnforcementState;
}
if (typeof json.managedRules === 'undefined') {
delete json.managedRules;
}
if (typeof json.recaptchaKeys === 'undefined') {
delete json.recaptchaKeys;
}

return json;
}
}
6 changes: 6 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export {
OAuthResponseType,
OIDCAuthProviderConfig,
OIDCUpdateAuthProviderRequest,
RecaptchaAction,
RecaptchaConfig,
RecaptchaKey,
RecaptchaKeyClientType,
RecaptchaManagedRule,
RecaptchaProviderEnforcementState,
SAMLAuthProviderConfig,
SAMLUpdateAuthProviderRequest,
SmsRegionConfig,
Expand Down
35 changes: 34 additions & 1 deletion src/auth/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig,
MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig,
MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig
MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig
} from './auth-config';

/**
Expand Down Expand Up @@ -59,6 +59,11 @@ export interface UpdateTenantRequest {
* The SMS configuration to update on the project.
*/
smsRegionConfig?: SmsRegionConfig;

/**
* The recaptcha configuration to update on the tenant.
*/
recaptchaConfig?: RecaptchaConfig;
}

/**
Expand All @@ -74,6 +79,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig?: RecaptchaConfig;
}

/** The tenant server response interface. */
Expand All @@ -86,6 +92,7 @@ export interface TenantServerResponse {
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig? : RecaptchaConfig;
}

/**
Expand Down Expand Up @@ -130,6 +137,10 @@ export class Tenant {
private readonly emailSignInConfig_?: EmailSignInConfig;
private readonly multiFactorConfig_?: MultiFactorAuthConfig;

/*
* The map conatining the reCAPTCHA config.
*/
private readonly recaptchaConfig_?: RecaptchaAuthConfig;
/**
* The SMS Regions Config to update a tenant.
* Configures the regions where users are allowed to send verification SMS.
Expand Down Expand Up @@ -169,6 +180,9 @@ export class Tenant {
if (typeof tenantOptions.smsRegionConfig !== 'undefined') {
request.smsRegionConfig = tenantOptions.smsRegionConfig;
}
if (typeof tenantOptions.recaptchaConfig !== 'undefined') {
request.recaptchaConfig = tenantOptions.recaptchaConfig;
}
return request;
}

Expand Down Expand Up @@ -203,6 +217,7 @@ export class Tenant {
multiFactorConfig: true,
testPhoneNumbers: true,
smsRegionConfig: true,
recaptchaConfig: true,
};
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
if (!validator.isNonNullObject(request)) {
Expand Down Expand Up @@ -253,6 +268,10 @@ export class Tenant {
if (typeof request.smsRegionConfig != 'undefined') {
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
}
// Validate reCAPTCHAConfig type if provided.
if (typeof request.recaptchaConfig !== 'undefined') {
RecaptchaAuthConfig.validate(request.recaptchaConfig);
}
}

/**
Expand Down Expand Up @@ -290,6 +309,9 @@ export class Tenant {
if (typeof response.smsRegionConfig !== 'undefined') {
this.smsRegionConfig = deepCopy(response.smsRegionConfig);
}
if (typeof response.recaptchaConfig !== 'undefined') {
this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
}
}

/**
Expand All @@ -306,6 +328,13 @@ export class Tenant {
return this.multiFactorConfig_;
}

/**
* The recaptcha config auth configuration of the current tenant.
*/
get recaptchaConfig(): RecaptchaConfig | undefined {
return this.recaptchaConfig_;
}

/**
* Returns a JSON-serializable representation of this object.
*
Expand All @@ -320,6 +349,7 @@ export class Tenant {
anonymousSignInEnabled: this.anonymousSignInEnabled,
testPhoneNumbers: this.testPhoneNumbers,
smsRegionConfig: deepCopy(this.smsRegionConfig),
recaptchaConfig: this.recaptchaConfig_?.toJSON(),
};
if (typeof json.multiFactorConfig === 'undefined') {
delete json.multiFactorConfig;
Expand All @@ -330,6 +360,9 @@ export class Tenant {
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
if (typeof json.recaptchaConfig === 'undefined') {
delete json.recaptchaConfig;
}
return json;
}
}
Expand Down
Loading

0 comments on commit 9dea070

Please sign in to comment.