Skip to content

Commit

Permalink
feat(auth): reCAPTCHA Public preview (#2129)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Xiaoshouzi-gh committed Apr 12, 2023
1 parent 0da72ef commit 0bf7d29
Show file tree
Hide file tree
Showing 11 changed files with 845 additions and 27 deletions.
33 changes: 33 additions & 0 deletions etc/firebase-admin.auth.api.md
Expand Up @@ -344,6 +344,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
// @public
export class ProjectConfig {
get multiFactorConfig(): MultiFactorConfig | undefined;
get recaptchaConfig(): RecaptchaConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}
Expand All @@ -362,6 +363,35 @@ export interface ProviderIdentifier {
providerUid: string;
}

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

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

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

// @public
export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID';

// @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 @@ -397,6 +427,7 @@ export class Tenant {
readonly displayName?: string;
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
get recaptchaConfig(): RecaptchaConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
readonly tenantId: string;
readonly testPhoneNumbers?: {
Expand Down Expand Up @@ -448,6 +479,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
// @public
export interface UpdateProjectConfigRequest {
multiFactorConfig?: MultiFactorConfig;
recaptchaConfig?: RecaptchaConfig;
smsRegionConfig?: SmsRegionConfig;
}

Expand All @@ -471,6 +503,7 @@ export interface UpdateTenantRequest {
displayName?: string;
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
recaptchaConfig?: RecaptchaConfig;
smsRegionConfig?: SmsRegionConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
Expand Down
224 changes: 224 additions & 0 deletions src/auth/auth-config.ts
Expand Up @@ -1722,3 +1722,227 @@ export class SmsRegionsAuthConfig {
}
}
}
/**
* Enforcement state of reCAPTCHA protection.
* - 'OFF': Unenforced.
* - 'AUDIT': Create assessment but don't enforce the result.
* - 'ENFORCE': Create assessment and enforce the result.
*/
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';

/**
* The actions to take for reCAPTCHA-protected requests.
* - 'BLOCK': The reCAPTCHA-protected request will be blocked.
*/
export type RecaptchaAction = 'BLOCK';

/**
* The config for a reCAPTCHA action rule.
*/
export interface RecaptchaManagedRule {
/**
* The action will be enforced if the reCAPTCHA score of a request is larger than endScore.
*/
endScore: number;
/**
* The action for reCAPTCHA-protected requests.
*/
action?: RecaptchaAction;
}

/**
* The key's platform type.
*/
export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID';

/**
* The reCAPTCHA key config.
*/
export interface RecaptchaKey {
/**
* The key's client platform type.
*/
type?: RecaptchaKeyClientType;

/**
* The reCAPTCHA site key.
*/
key: string;
}

/**
* The request interface for updating a reCAPTCHA Config.
* By enabling reCAPTCHA Enterprise Integration you are
* agreeing to reCAPTCHA Enterprise
* {@link https://cloud.google.com/terms/service-terms | Term of Service}.
*/
export interface RecaptchaConfig {
/**
* The enforcement state of the email password provider.
*/
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
/**
* The reCAPTCHA managed rules.
*/
managedRules?: RecaptchaManagedRule[];

/**
* The reCAPTCHA keys.
*/
recaptchaKeys?: RecaptchaKey[];

/**
* Whether to use account defender for reCAPTCHA assessment.
* The default value is false.
*/
useAccountDefender?: boolean;
}

export class RecaptchaAuthConfig implements RecaptchaConfig {
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
public readonly managedRules?: RecaptchaManagedRule[];
public readonly recaptchaKeys?: RecaptchaKey[];
public readonly useAccountDefender?: boolean;

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

/**
* 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,
useAccountDefender: 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);
});
}

if (typeof options.useAccountDefender != 'undefined') {
if (!validator.isBoolean(options.useAccountDefender)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.useAccountDefender" must be a boolean value".',
);
}
}
}

/**
* 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),
useAccountDefender: this.useAccountDefender,
}

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

if (typeof json.useAccountDefender === 'undefined') {
delete json.useAccountDefender;
}

return json;
}
}
6 changes: 6 additions & 0 deletions src/auth/index.ts
Expand Up @@ -84,6 +84,12 @@ export {
OAuthResponseType,
OIDCAuthProviderConfig,
OIDCUpdateAuthProviderRequest,
RecaptchaAction,
RecaptchaConfig,
RecaptchaKey,
RecaptchaKeyClientType,
RecaptchaManagedRule,
RecaptchaProviderEnforcementState,
SAMLAuthProviderConfig,
SAMLUpdateAuthProviderRequest,
SmsRegionConfig,
Expand Down
6 changes: 1 addition & 5 deletions src/auth/project-config-manager.ts
Expand Up @@ -20,14 +20,10 @@ import {
} from './auth-api-request';

/**
* Defines the project config manager used to help manage project config related operations.
* This includes:
* <ul>
* <li>The ability to update and get project config.</li>
* Manages (gets and updates) the current project config.
*/
export class ProjectConfigManager {
private readonly authRequestHandler: AuthRequestHandler;

/**
* Initializes a ProjectConfigManager instance for a specified FirebaseApp.
*
Expand Down

0 comments on commit 0bf7d29

Please sign in to comment.