Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): reCAPTCHA Public preview #2129

Merged
merged 10 commits into from Apr 12, 2023
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