From 1cd6fe5002ed88ade0f967c755c1b507e4033bee Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 22 Feb 2022 10:51:56 -0800 Subject: [PATCH 01/14] Support reCaptcha config /create update on tenants. - Updated reCaptcha config type align with backend API. - Support create and update tenants with reCaptcha config. - Added reCaptcha unit tests on tenants. --- src/auth/auth-config.ts | 228 +++++++++++++++++++++++++++++- src/auth/index.ts | 3 + src/auth/tenant.ts | 44 +++++- test/unit/auth/tenant.spec.ts | 256 +++++++++++++++++++++++++++++++++- 4 files changed, 519 insertions(+), 12 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index a640cc3534..af91a1ba8d 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1460,6 +1460,54 @@ export class OIDCConfig implements OIDCAuthProviderConfig { */ export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; +export interface ProviderRecaptchaConfig { + enforcementState: RecaptchaProviderEnforcementState; +} + +export class ProviderRecaptchaAuthConfig implements ProviderRecaptchaConfig { + public readonly enforcementState: RecaptchaProviderEnforcementState; + + public static validate(options: ProviderRecaptchaConfig): void { + const validKeys = { + enforcementState: true, + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"ProviderRecaptchaConfig" 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 ProviderRecaptchaConfig parameter.`, + ); + } + } + + // Validate content. + if (typeof options.enforcementState !== 'undefined' && + options.enforcementState !== 'OFF' && + options.enforcementState !== 'AUDIT' && + options.enforcementState !== 'ENFORCE') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', + ); + } + } + constructor(response: ProviderRecaptchaConfig) { + if (typeof response.enforcementState === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid provider-reCAPTCHA configuration response'); + } + this.enforcementState = response.enforcementState; + } +} + /** * The actions for reCAPTCHA-protected requests. * - 'BLOCK': The reCAPTCHA-protected request will be blocked. @@ -1469,7 +1517,7 @@ export type RecaptchaAction = 'BLOCK'; /** * The config for a reCAPTCHA action rule. */ -export interface RecaptchaManagedRule { +export interface RuleConfig { /** * The action will be enforced if the reCAPTCHA score of a request is larger than endScore. */ @@ -1480,6 +1528,100 @@ export interface RecaptchaManagedRule { action?: RecaptchaAction; } +export class AuthRuleConfig implements RuleConfig{ + public readonly endScore: number; + public readonly action: RecaptchaAction; + + public static validate(options: RuleConfig): void { + const validKeys = { + endScore: true, + action: true, + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RuleConfig" 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 RuleConfig parameter.`, + ); + } + } + + // Validate content. + if (typeof options.action !== 'undefined' && + options.action !== 'BLOCK') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RuleConfig.action" must be "BLOCK".', + ); + } + } + + constructor(response: RuleConfig) { + if (typeof response.action === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid provider-reCAPTCHA configuration response'); + } + this.action = response.action; + if (response.endScore !== undefined) { + this.endScore = response.endScore; + } + } +} + +export interface RecaptchaManagedRules { + ruleConfigs: RuleConfig[]; +} + +export class RecaptchaAuthManagedRules implements RecaptchaManagedRules{ + public readonly ruleConfigs: RuleConfig[]; + + public static validate(options: RecaptchaManagedRules): void { + const validKeys = { + ruleConfigs: true, + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RuleConfig" 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 RecaptchaManagedRules parameter.`, + ); + } + } + + if (typeof options.ruleConfigs !== 'undefined') { + // Validate array + if (!validator.isArray(options.ruleConfigs)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".', + ); + } + + // Validate each rule of the array + options.ruleConfigs.forEach((ruleConfig) => { + AuthRuleConfig.validate(ruleConfig); + }); + } + + } +} + + /** * The key's platform type: only web supported now. */ @@ -1488,31 +1630,103 @@ export type RecaptchaKeyClientType = 'WEB'; /** * The reCAPTCHA key config. */ -export interface RecaptchaKey { +export interface RecaptchaKeyConfig { /** * The key's client platform type. */ - type?: RecaptchaKeyClientType; + clientType?: RecaptchaKeyClientType; /** * The reCAPTCHA site key. */ - key: string; + recaptchaKey: string; } export interface RecaptchaConfig { /** * The enforcement state of email password provider. */ - emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; /** * The reCAPTCHA managed rules. */ - managedRules: RecaptchaManagedRule[]; + recaptchaManagedRules?: RecaptchaManagedRules; /** * The reCAPTCHA keys. */ - recaptchaKeys?: RecaptchaKey[]; + recaptchaKeyConfig?: RecaptchaKeyConfig[]; +} + +export class RecaptchaConfigAuth implements RecaptchaConfig { + public readonly emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + public readonly recaptchaManagedRules?: RecaptchaManagedRules; + public readonly recaptchaKeyConfig?: RecaptchaKeyConfig[]; + + constructor(emailPasswordRecaptchaConfig: ProviderRecaptchaConfig | undefined, + recaptchaManagedRules: RecaptchaManagedRules | undefined, + recaptchaKeyConfig: RecaptchaKeyConfig[] | undefined) { + if (emailPasswordRecaptchaConfig !== undefined) { + this.emailPasswordRecaptchaConfig = emailPasswordRecaptchaConfig; + } + if (recaptchaManagedRules !== undefined) { + this.recaptchaManagedRules = recaptchaManagedRules; + } + if (recaptchaKeyConfig !== undefined) { + this.recaptchaKeyConfig = recaptchaKeyConfig; + } + } + + public static validate(options: RecaptchaConfig): void { + const validKeys = { + emailPasswordRecaptchaConfig: true, + recaptchaManagedRules: true, + recaptchaKeyConfig: 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.`, + ); + } + } + + if (options.emailPasswordRecaptchaConfig !== undefined) { + ProviderRecaptchaAuthConfig.validate(options.emailPasswordRecaptchaConfig); + } + + if (options.recaptchaManagedRules !== undefined) { + RecaptchaAuthManagedRules.validate(options.recaptchaManagedRules); + } + } + + public toJSON(): object { + const json: any = { + emailPasswordRecaptchaConfig: deepCopy(this.emailPasswordRecaptchaConfig), + recaptchaManagedRules: deepCopy(this.recaptchaManagedRules), + recaptchaKeyConfig: deepCopy(this.recaptchaKeyConfig) + } + + if (typeof json.recaptchaManagedRules === 'undefined') { + delete json.recaptchaManagedRules; + } + if (typeof json.emailPasswordRecaptchaConfig === 'undefined') { + delete json.emailPasswordRecaptchaConfig; + } + if (typeof json.recaptchaKeyConfig === 'undefined') { + delete json.recaptchaKeyConfig; + } + + return json; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 0b92a796cf..88cb939e81 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -79,6 +79,9 @@ export { OAuthResponseType, OIDCAuthProviderConfig, OIDCUpdateAuthProviderRequest, + ProviderRecaptchaConfig, + RecaptchaKeyConfig, + RecaptchaManagedRules, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, UserProvider, diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index e489fa3b09..48ad5351a8 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,7 +21,8 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, + MultiFactorAuthConfig, ProviderRecaptchaConfig, RecaptchaManagedRules, + RecaptchaKeyConfig, RecaptchaConfigAuth, RecaptchaConfig } from './auth-config'; /** @@ -54,6 +55,8 @@ export interface UpdateTenantRequest { * Passing null clears the previously save phone number / code pairs. */ testPhoneNumbers?: { [phoneNumber: string]: string } | null; + + recaptchaConfig?: RecaptchaConfig; } /** @@ -68,6 +71,8 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + recaptchaManagedRules?: RecaptchaManagedRules; } /** The tenant server response interface. */ @@ -79,6 +84,9 @@ export interface TenantServerResponse { enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + recaptchaManagedRules?: RecaptchaManagedRules; + recaptchaKeyConfig?: RecaptchaKeyConfig[]; } /** @@ -123,6 +131,10 @@ export class Tenant { private readonly emailSignInConfig_?: EmailSignInConfig; private readonly multiFactorConfig_?: MultiFactorAuthConfig; + /* + * The map conatining the reCAPTCHA config. + */ + private readonly recaptchaConfig_?: RecaptchaConfigAuth; /** * Builds the corresponding server request for a TenantOptions object. * @@ -152,6 +164,13 @@ export class Tenant { // null will clear existing test phone numbers. Translate to empty object. request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; } + // reCAPTCHA Key Config cannot be updated. + if (typeof tenantOptions.recaptchaConfig?.emailPasswordRecaptchaConfig !== 'undefined') { + request.emailPasswordRecaptchaConfig = tenantOptions.recaptchaConfig.emailPasswordRecaptchaConfig; + } + if (typeof tenantOptions.recaptchaConfig?.recaptchaManagedRules !== 'undefined') { + request.recaptchaManagedRules = tenantOptions.recaptchaConfig.recaptchaManagedRules; + } return request; } @@ -185,6 +204,7 @@ export class Tenant { anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, + recaptchaConfig: true }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -231,6 +251,10 @@ export class Tenant { // This will throw an error if invalid. MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); } + // Validate reCAPTCHAConfig type if provided. + if (typeof request.recaptchaConfig !== 'undefined') { + RecaptchaConfigAuth.validate(request.recaptchaConfig); + } } /** @@ -265,6 +289,13 @@ export class Tenant { if (typeof response.testPhoneNumbers !== 'undefined') { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } + if (typeof response.emailPasswordRecaptchaConfig !== 'undefined' + || typeof response.recaptchaManagedRules !== 'undefined' + || typeof response.recaptchaKeyConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaConfigAuth( + response.emailPasswordRecaptchaConfig, response.recaptchaManagedRules, + response.recaptchaKeyConfig); + } } /** @@ -281,6 +312,13 @@ export class Tenant { return this.multiFactorConfig_; } + /** + * The recaptcha config auth configuration of the current tenant. + */ + get recaptchaConfig(): RecaptchaConfigAuth | undefined { + return this.recaptchaConfig_; + } + /** * Returns a JSON-serializable representation of this object. * @@ -294,6 +332,7 @@ export class Tenant { multiFactorConfig: this.multiFactorConfig_?.toJSON(), anonymousSignInEnabled: this.anonymousSignInEnabled, testPhoneNumbers: this.testPhoneNumbers, + recaptchaConfig: this.recaptchaConfig_?.toJSON(), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -301,6 +340,9 @@ export class Tenant { if (typeof json.testPhoneNumbers === 'undefined') { delete json.testPhoneNumbers; } + if (typeof json.recaptchaConfig === 'undefined') { + delete json.recaptchaConfig; + } return json; } } diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 0f14856faa..c1b86e3e52 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { EmailSignInConfig, MultiFactorAuthConfig } from '../../../src/auth/auth-config'; +import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaConfigAuth } from '../../../src/auth/auth-config'; import { TenantServerResponse } from '../../../src/auth/tenant'; import { CreateTenantRequest, UpdateTenantRequest, EmailSignInProviderConfig, Tenant, @@ -79,6 +79,59 @@ describe('Tenant', () => { }, }; + const serverRequestWithRecaptcha: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + emailPasswordRecaptchaConfig: { + enforcementState: 'OFF' + }, + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }]}, + recaptchaKeyConfig: [ { + clientType: 'WEB', + recaptchaKey: "test-key-1" } + ], + }; + + const clientRequestWithRecaptcha: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + recaptchaConfig: { + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }]}, + emailPasswordRecaptchaConfig: { + enforcementState: 'OFF' + }, + } + }; + describe('buildServerRequest()', () => { const createRequest = true; @@ -122,6 +175,90 @@ describe('Tenant', () => { }).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".'); }); + it('should throw on null RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw(`"invalidParameter" is not a valid RecaptchaConfig parameter.`); + }); + + it('should throw on null ProviderRecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw(`"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.`); + }); + + it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + it('should throw on null RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RuleConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw(`"invalidParameter" is not a valid RecaptchaManagedRules parameter.`); + }); + + it('should throw on non-array RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); + }); + + it('should throw on invalid RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{'score': 0.1, 'action': 'BLOCK'}]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw(`"score" is not a valid RuleConfig parameter.`); + }); + + it('should throw on invalid RuleConfig.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{'endScore': 0.1, 'action': 'ALLOW'}]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }); + it('should throw on invalid testPhoneNumbers attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.testPhoneNumbers = 'invalid'; @@ -142,7 +279,7 @@ describe('Tenant', () => { }); it('should not throw on valid client request object', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha); expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).not.to.throw; @@ -212,6 +349,90 @@ describe('Tenant', () => { }).to.throw('"invalid" is not a valid "AuthFactorType".',); }); + it('should throw on null RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"invalidParameter" is not a valid RecaptchaConfig parameter.`); + }); + + it('should throw on null ProviderRecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.`); + }); + + it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + it('should throw on null RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RuleConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaManagedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"invalidParameter" is not a valid RecaptchaManagedRules parameter.`); + }); + + it('should throw on non-array RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); + }); + + it('should throw on invalid RuleConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{'score': 0.1, 'action': 'BLOCK'}]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"score" is not a valid RuleConfig parameter.`); + }); + + it('should throw on invalid RuleConfig.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{'endScore': 0.1, 'action': 'ALLOW'}]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }); + it('should throw on invalid testPhoneNumbers attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.testPhoneNumbers = { 'invalid': '123456' }; @@ -309,6 +530,25 @@ describe('Tenant', () => { expect(tenant.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig); }); + it('should set readonly property recaptchaConfig', () => { + const serverRequestWithRecaptchaCopy: TenantServerResponse = + deepCopy(serverRequestWithRecaptcha); + const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy); + const expectedRecaptchaConfig = new RecaptchaConfigAuth( + { enforcementState: 'OFF' }, + { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }]}, + [{ + clientType: 'WEB', + recaptchaKey: "test-key-1" } + ], + ); + expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); + }); + it('should set readonly property testPhoneNumbers', () => { expect(tenant.testPhoneNumbers).to.deep.equal( deepCopy(clientRequest.testPhoneNumbers)); @@ -340,7 +580,7 @@ describe('Tenant', () => { }); describe('toJSON()', () => { - const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest); + const serverRequestCopy: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); it('should return the expected object representation of a tenant', () => { expect(new Tenant(serverRequestCopy).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', @@ -352,13 +592,21 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + recaptchaConfig: { + emailPasswordRecaptchaConfig: deepCopy(serverRequestWithRecaptcha.emailPasswordRecaptchaConfig), + recaptchaKeyConfig: deepCopy(serverRequestWithRecaptcha.recaptchaKeyConfig), + recaptchaManagedRules: deepCopy(serverRequestWithRecaptcha.recaptchaManagedRules), + } }); }); it('should not populate optional fields if not available', () => { - const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); + const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; + delete serverRequestCopyWithoutMfa.recaptchaKeyConfig; + delete serverRequestCopyWithoutMfa.emailPasswordRecaptchaConfig; + delete serverRequestCopyWithoutMfa.recaptchaManagedRules; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', From 4bdd00baac456a60bb56f2fd82c1bc3930d912c9 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 22 Feb 2022 11:38:41 -0800 Subject: [PATCH 02/14] fix lint issue --- test/unit/auth/tenant.spec.ts | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index c1b86e3e52..07687c3e7e 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -99,10 +99,10 @@ describe('Tenant', () => { ruleConfigs: [{ endScore: 0.2, action: 'BLOCK' - }]}, + }] }, recaptchaKeyConfig: [ { clientType: 'WEB', - recaptchaKey: "test-key-1" } + recaptchaKey: 'test-key-1' } ], }; @@ -125,7 +125,7 @@ describe('Tenant', () => { ruleConfigs: [{ endScore: 0.2, action: 'BLOCK' - }]}, + }] }, emailPasswordRecaptchaConfig: { enforcementState: 'OFF' }, @@ -188,7 +188,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"invalidParameter" is not a valid RecaptchaConfig parameter.`); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); it('should throw on null ProviderRecaptchaConfig attribute', () => { @@ -202,16 +202,16 @@ describe('Tenant', () => { it('should throw on invalid ProviderRecaptchaConfig attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.`); + }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); }); it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); @@ -230,7 +230,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"invalidParameter" is not a valid RecaptchaManagedRules parameter.`); + }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); }); it('should throw on non-array RuleConfig attribute', () => { @@ -244,16 +244,16 @@ describe('Tenant', () => { it('should throw on invalid RuleConfig attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = - [{'score': 0.1, 'action': 'BLOCK'}]; + [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"score" is not a valid RuleConfig parameter.`); + }).to.throw('"score" is not a valid RuleConfig parameter.'); }); it('should throw on invalid RuleConfig.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = - [{'endScore': 0.1, 'action': 'ALLOW'}]; + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"RuleConfig.action" must be "BLOCK".'); @@ -362,7 +362,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"invalidParameter" is not a valid RecaptchaConfig parameter.`); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); it('should throw on null ProviderRecaptchaConfig attribute', () => { @@ -376,16 +376,16 @@ describe('Tenant', () => { it('should throw on invalid ProviderRecaptchaConfig attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.`); + }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); }); it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); @@ -404,7 +404,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"invalidParameter" is not a valid RecaptchaManagedRules parameter.`); + }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); }); it('should throw on non-array RuleConfig attribute', () => { @@ -418,16 +418,16 @@ describe('Tenant', () => { it('should throw on invalid RuleConfig attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = - [{'score': 0.1, 'action': 'BLOCK'}]; + [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"score" is not a valid RuleConfig parameter.`); + }).to.throw('"score" is not a valid RuleConfig parameter.'); }); it('should throw on invalid RuleConfig.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = - [{'endScore': 0.1, 'action': 'ALLOW'}]; + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"RuleConfig.action" must be "BLOCK".'); @@ -540,10 +540,10 @@ describe('Tenant', () => { ruleConfigs: [{ endScore: 0.2, action: 'BLOCK' - }]}, + }] }, [{ clientType: 'WEB', - recaptchaKey: "test-key-1" } + recaptchaKey: 'test-key-1' } ], ); expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); From f98cd0ed80c325c409add6f7de92abf7f811937a Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 22 Feb 2022 13:45:32 -0800 Subject: [PATCH 03/14] export Recaptcha Config --- src/auth/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/auth/index.ts b/src/auth/index.ts index 88cb939e81..9178868fdc 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -80,8 +80,14 @@ export { OIDCAuthProviderConfig, OIDCUpdateAuthProviderRequest, ProviderRecaptchaConfig, + RecaptchaAction, + RecaptchaConfig, + RecaptchaConfigAuth, RecaptchaKeyConfig, + RecaptchaKeyClientType, RecaptchaManagedRules, + RecaptchaProviderEnforcementState, + RuleConfig, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, UserProvider, From 766adc5f2d57d92b7b8bd41d4cd4f8efe95c9d54 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 22 Feb 2022 13:56:41 -0800 Subject: [PATCH 04/14] update public API signature for updateTenant with recaptcha config --- etc/firebase-admin.auth.api.md | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 36b2dcf686..de2914a0d2 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -264,6 +264,61 @@ export interface ProviderIdentifier { providerUid: string; } +// @public (undocumented) +export interface ProviderRecaptchaConfig { + // (undocumented) + enforcementState: RecaptchaProviderEnforcementState; +} + +// @public +export type RecaptchaAction = 'BLOCK'; + +// @public (undocumented) +export interface RecaptchaConfig { + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + recaptchaKeyConfig?: RecaptchaKeyConfig[]; + recaptchaManagedRules?: RecaptchaManagedRules; +} + +// @public (undocumented) +export class RecaptchaConfigAuth implements RecaptchaConfig { + constructor(emailPasswordRecaptchaConfig: ProviderRecaptchaConfig | undefined, recaptchaManagedRules: RecaptchaManagedRules | undefined, recaptchaKeyConfig: RecaptchaKeyConfig[] | undefined); + // (undocumented) + readonly emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + // (undocumented) + readonly recaptchaKeyConfig?: RecaptchaKeyConfig[]; + // (undocumented) + readonly recaptchaManagedRules?: RecaptchaManagedRules; + // (undocumented) + toJSON(): object; + // (undocumented) + static validate(options: RecaptchaConfig): void; +} + +// @public +export type RecaptchaKeyClientType = 'WEB'; + +// @public +export interface RecaptchaKeyConfig { + clientType?: RecaptchaKeyClientType; + recaptchaKey: string; +} + +// @public (undocumented) +export interface RecaptchaManagedRules { + // (undocumented) + ruleConfigs: RuleConfig[]; +} + +// @public +export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; + +// @public +export interface RuleConfig { + action?: RecaptchaAction; + endScore: number; +} + // @public export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { callbackURL?: string; @@ -296,6 +351,7 @@ export class Tenant { readonly displayName?: string; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; + get recaptchaConfig(): RecaptchaConfigAuth | undefined; readonly tenantId: string; readonly testPhoneNumbers?: { [phoneNumber: string]: string; @@ -358,6 +414,8 @@ export interface UpdateTenantRequest { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; + // (undocumented) + recaptchaConfig?: RecaptchaConfig; testPhoneNumbers?: { [phoneNumber: string]: string; } | null; From 930a277be6c4495cc8a7b4c0f13c68fde16cfdfe Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 24 Feb 2022 11:14:40 -0800 Subject: [PATCH 05/14] un-export unnecessary class --- etc/firebase-admin.auth.api.md | 17 +---------------- src/auth/index.ts | 1 - src/auth/tenant.ts | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index de2914a0d2..102d0a8178 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -280,21 +280,6 @@ export interface RecaptchaConfig { recaptchaManagedRules?: RecaptchaManagedRules; } -// @public (undocumented) -export class RecaptchaConfigAuth implements RecaptchaConfig { - constructor(emailPasswordRecaptchaConfig: ProviderRecaptchaConfig | undefined, recaptchaManagedRules: RecaptchaManagedRules | undefined, recaptchaKeyConfig: RecaptchaKeyConfig[] | undefined); - // (undocumented) - readonly emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - // (undocumented) - readonly recaptchaKeyConfig?: RecaptchaKeyConfig[]; - // (undocumented) - readonly recaptchaManagedRules?: RecaptchaManagedRules; - // (undocumented) - toJSON(): object; - // (undocumented) - static validate(options: RecaptchaConfig): void; -} - // @public export type RecaptchaKeyClientType = 'WEB'; @@ -351,7 +336,7 @@ export class Tenant { readonly displayName?: string; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; - get recaptchaConfig(): RecaptchaConfigAuth | undefined; + get recaptchaConfig(): RecaptchaConfig | undefined; readonly tenantId: string; readonly testPhoneNumbers?: { [phoneNumber: string]: string; diff --git a/src/auth/index.ts b/src/auth/index.ts index 9178868fdc..f194dcb654 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -82,7 +82,6 @@ export { ProviderRecaptchaConfig, RecaptchaAction, RecaptchaConfig, - RecaptchaConfigAuth, RecaptchaKeyConfig, RecaptchaKeyClientType, RecaptchaManagedRules, diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 48ad5351a8..8da3b0bf49 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -315,7 +315,7 @@ export class Tenant { /** * The recaptcha config auth configuration of the current tenant. */ - get recaptchaConfig(): RecaptchaConfigAuth | undefined { + get recaptchaConfig(): RecaptchaConfig | undefined { return this.recaptchaConfig_; } From 6452fe1777e665bd10977cfe3f3b248cfef669c6 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 3 Mar 2022 11:42:50 -0800 Subject: [PATCH 06/14] refactor recaptcha config types to compile with backend API schema changes --- etc/firebase-admin.auth.api.md | 34 ++---- src/auth/auth-api-request.ts | 2 + src/auth/auth-config.ts | 198 +++++++++++---------------------- src/auth/index.ts | 6 +- src/auth/tenant.ts | 26 ++--- test/unit/auth/tenant.spec.ts | 175 ++++++++++------------------- 6 files changed, 142 insertions(+), 299 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 102d0a8178..abcd25d190 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -264,46 +264,34 @@ export interface ProviderIdentifier { providerUid: string; } -// @public (undocumented) -export interface ProviderRecaptchaConfig { - // (undocumented) - enforcementState: RecaptchaProviderEnforcementState; -} - // @public export type RecaptchaAction = 'BLOCK'; // @public (undocumented) export interface RecaptchaConfig { - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - recaptchaKeyConfig?: RecaptchaKeyConfig[]; - recaptchaManagedRules?: RecaptchaManagedRules; + emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + managedRules?: RecaptchaManagedRule[]; + recaptchaKeys?: RecaptchaKey[]; } // @public -export type RecaptchaKeyClientType = 'WEB'; - -// @public -export interface RecaptchaKeyConfig { - clientType?: RecaptchaKeyClientType; - recaptchaKey: string; -} - -// @public (undocumented) -export interface RecaptchaManagedRules { - // (undocumented) - ruleConfigs: RuleConfig[]; +export interface RecaptchaKey { + key: string; + type?: RecaptchaKeyClientType; } // @public -export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; +export type RecaptchaKeyClientType = 'WEB'; // @public -export interface RuleConfig { +export interface RecaptchaManagedRule { action?: RecaptchaAction; endScore: number; } +// @public +export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; + // @public export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { callbackURL?: string; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 13018337da..2bddf2cf85 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1896,6 +1896,8 @@ export abstract class AbstractAuthRequestHandler { requestValidator(requestData); } // Process request. + console.log(requestData); + console.log(url); const req: HttpRequestConfig = { method: apiSettings.getHttpMethod(), url, diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index af91a1ba8d..e64b87d9f6 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1460,54 +1460,6 @@ export class OIDCConfig implements OIDCAuthProviderConfig { */ export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; -export interface ProviderRecaptchaConfig { - enforcementState: RecaptchaProviderEnforcementState; -} - -export class ProviderRecaptchaAuthConfig implements ProviderRecaptchaConfig { - public readonly enforcementState: RecaptchaProviderEnforcementState; - - public static validate(options: ProviderRecaptchaConfig): void { - const validKeys = { - enforcementState: true, - } - if (!validator.isNonNullObject(options)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"ProviderRecaptchaConfig" 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 ProviderRecaptchaConfig parameter.`, - ); - } - } - - // Validate content. - if (typeof options.enforcementState !== 'undefined' && - options.enforcementState !== 'OFF' && - options.enforcementState !== 'AUDIT' && - options.enforcementState !== 'ENFORCE') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', - ); - } - } - constructor(response: ProviderRecaptchaConfig) { - if (typeof response.enforcementState === 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Invalid provider-reCAPTCHA configuration response'); - } - this.enforcementState = response.enforcementState; - } -} - /** * The actions for reCAPTCHA-protected requests. * - 'BLOCK': The reCAPTCHA-protected request will be blocked. @@ -1517,7 +1469,7 @@ export type RecaptchaAction = 'BLOCK'; /** * The config for a reCAPTCHA action rule. */ -export interface RuleConfig { +export interface RecaptchaManagedRule { /** * The action will be enforced if the reCAPTCHA score of a request is larger than endScore. */ @@ -1528,11 +1480,11 @@ export interface RuleConfig { action?: RecaptchaAction; } -export class AuthRuleConfig implements RuleConfig{ +export class ManagedRuleAuth implements RecaptchaManagedRule{ public readonly endScore: number; public readonly action: RecaptchaAction; - public static validate(options: RuleConfig): void { + public static validate(options: RecaptchaManagedRule): void { const validKeys = { endScore: true, action: true, @@ -1540,7 +1492,7 @@ export class AuthRuleConfig implements RuleConfig{ if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, - '"RuleConfig" must be a non-null object.', + '"RecaptchaManagedRule" must be a non-null object.', ); } // Check for unsupported top level attributes. @@ -1548,7 +1500,7 @@ export class AuthRuleConfig implements RuleConfig{ if (!(key in validKeys)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, - `"${key}" is not a valid RuleConfig parameter.`, + `"${key}" is not a valid RecaptchaManagedRule parameter.`, ); } } @@ -1558,12 +1510,12 @@ export class AuthRuleConfig implements RuleConfig{ options.action !== 'BLOCK') { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, - '"RuleConfig.action" must be "BLOCK".', + '"RecaptchaManagedRule.action" must be "BLOCK".', ); } } - constructor(response: RuleConfig) { + constructor(response: RecaptchaManagedRule) { if (typeof response.action === 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, @@ -1576,52 +1528,6 @@ export class AuthRuleConfig implements RuleConfig{ } } -export interface RecaptchaManagedRules { - ruleConfigs: RuleConfig[]; -} - -export class RecaptchaAuthManagedRules implements RecaptchaManagedRules{ - public readonly ruleConfigs: RuleConfig[]; - - public static validate(options: RecaptchaManagedRules): void { - const validKeys = { - ruleConfigs: true, - } - if (!validator.isNonNullObject(options)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"RuleConfig" 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 RecaptchaManagedRules parameter.`, - ); - } - } - - if (typeof options.ruleConfigs !== 'undefined') { - // Validate array - if (!validator.isArray(options.ruleConfigs)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".', - ); - } - - // Validate each rule of the array - options.ruleConfigs.forEach((ruleConfig) => { - AuthRuleConfig.validate(ruleConfig); - }); - } - - } -} - - /** * The key's platform type: only web supported now. */ @@ -1630,59 +1536,57 @@ export type RecaptchaKeyClientType = 'WEB'; /** * The reCAPTCHA key config. */ -export interface RecaptchaKeyConfig { +export interface RecaptchaKey { /** * The key's client platform type. */ - clientType?: RecaptchaKeyClientType; + type?: RecaptchaKeyClientType; /** * The reCAPTCHA site key. */ - recaptchaKey: string; + key: string; } export interface RecaptchaConfig { /** * The enforcement state of email password provider. */ - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; /** * The reCAPTCHA managed rules. */ - recaptchaManagedRules?: RecaptchaManagedRules; + managedRules?: RecaptchaManagedRule[]; /** * The reCAPTCHA keys. */ - recaptchaKeyConfig?: RecaptchaKeyConfig[]; + recaptchaKeys?: RecaptchaKey[]; } export class RecaptchaConfigAuth implements RecaptchaConfig { - public readonly emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - public readonly recaptchaManagedRules?: RecaptchaManagedRules; - public readonly recaptchaKeyConfig?: RecaptchaKeyConfig[]; + public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + public readonly managedRules?: RecaptchaManagedRule[]; + public readonly recaptchaKeys?: RecaptchaKey[]; - constructor(emailPasswordRecaptchaConfig: ProviderRecaptchaConfig | undefined, - recaptchaManagedRules: RecaptchaManagedRules | undefined, - recaptchaKeyConfig: RecaptchaKeyConfig[] | undefined) { - if (emailPasswordRecaptchaConfig !== undefined) { - this.emailPasswordRecaptchaConfig = emailPasswordRecaptchaConfig; + constructor(recaptchaConfig: RecaptchaConfig | undefined) { + if (recaptchaConfig?.emailPasswordEnforcementState !== undefined) { + this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; } - if (recaptchaManagedRules !== undefined) { - this.recaptchaManagedRules = recaptchaManagedRules; + if (recaptchaConfig?.managedRules !== undefined) { + this.managedRules = recaptchaConfig.managedRules; } - if (recaptchaKeyConfig !== undefined) { - this.recaptchaKeyConfig = recaptchaKeyConfig; + if (recaptchaConfig?.recaptchaKeys !== undefined) { + this.recaptchaKeys = recaptchaConfig.recaptchaKeys; } } public static validate(options: RecaptchaConfig): void { const validKeys = { - emailPasswordRecaptchaConfig: true, - recaptchaManagedRules: true, - recaptchaKeyConfig: true, + emailPasswordEnforcementState: true, + managedRules: true, + recaptchaKeys: true, }; if (!validator.isNonNullObject(options)) { @@ -1701,30 +1605,54 @@ export class RecaptchaConfigAuth implements RecaptchaConfig { } } - if (options.emailPasswordRecaptchaConfig !== undefined) { - ProviderRecaptchaAuthConfig.validate(options.emailPasswordRecaptchaConfig); + // Validation + if (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 (options.recaptchaManagedRules !== undefined) { - RecaptchaAuthManagedRules.validate(options.recaptchaManagedRules); + 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) => { + ManagedRuleAuth.validate(managedRule); + }); } } public toJSON(): object { const json: any = { - emailPasswordRecaptchaConfig: deepCopy(this.emailPasswordRecaptchaConfig), - recaptchaManagedRules: deepCopy(this.recaptchaManagedRules), - recaptchaKeyConfig: deepCopy(this.recaptchaKeyConfig) + emailPasswordEnforcementState: this.emailPasswordEnforcementState, + managedRules: deepCopy(this.managedRules), + recaptchaKeys: deepCopy(this.recaptchaKeys) } - if (typeof json.recaptchaManagedRules === 'undefined') { - delete json.recaptchaManagedRules; + if (typeof json.emailPasswordEnforcementState === 'undefined') { + delete json.emailPasswordEnforcementState; } - if (typeof json.emailPasswordRecaptchaConfig === 'undefined') { - delete json.emailPasswordRecaptchaConfig; + if (typeof json.managedRules === 'undefined') { + delete json.managedRules; } - if (typeof json.recaptchaKeyConfig === 'undefined') { - delete json.recaptchaKeyConfig; + if (typeof json.recaptchaKeys === 'undefined') { + delete json.recaptchaKeys; } return json; diff --git a/src/auth/index.ts b/src/auth/index.ts index f194dcb654..978cdfe8a1 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -79,14 +79,12 @@ export { OAuthResponseType, OIDCAuthProviderConfig, OIDCUpdateAuthProviderRequest, - ProviderRecaptchaConfig, RecaptchaAction, RecaptchaConfig, - RecaptchaKeyConfig, + RecaptchaKey, RecaptchaKeyClientType, - RecaptchaManagedRules, + RecaptchaManagedRule, RecaptchaProviderEnforcementState, - RuleConfig, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, UserProvider, diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 8da3b0bf49..914dde0070 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,8 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, ProviderRecaptchaConfig, RecaptchaManagedRules, - RecaptchaKeyConfig, RecaptchaConfigAuth, RecaptchaConfig + MultiFactorAuthConfig, RecaptchaConfigAuth, RecaptchaConfig } from './auth-config'; /** @@ -71,8 +70,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - recaptchaManagedRules?: RecaptchaManagedRules; + recaptchaConfig? : RecaptchaConfig; } /** The tenant server response interface. */ @@ -84,9 +82,7 @@ export interface TenantServerResponse { enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - recaptchaManagedRules?: RecaptchaManagedRules; - recaptchaKeyConfig?: RecaptchaKeyConfig[]; + recaptchaConfig? : RecaptchaConfig; } /** @@ -164,12 +160,8 @@ export class Tenant { // null will clear existing test phone numbers. Translate to empty object. request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; } - // reCAPTCHA Key Config cannot be updated. - if (typeof tenantOptions.recaptchaConfig?.emailPasswordRecaptchaConfig !== 'undefined') { - request.emailPasswordRecaptchaConfig = tenantOptions.recaptchaConfig.emailPasswordRecaptchaConfig; - } - if (typeof tenantOptions.recaptchaConfig?.recaptchaManagedRules !== 'undefined') { - request.recaptchaManagedRules = tenantOptions.recaptchaConfig.recaptchaManagedRules; + if (typeof tenantOptions.recaptchaConfig !== 'undefined') { + request.recaptchaConfig = tenantOptions.recaptchaConfig; } return request; } @@ -289,12 +281,8 @@ export class Tenant { if (typeof response.testPhoneNumbers !== 'undefined') { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } - if (typeof response.emailPasswordRecaptchaConfig !== 'undefined' - || typeof response.recaptchaManagedRules !== 'undefined' - || typeof response.recaptchaKeyConfig !== 'undefined') { - this.recaptchaConfig_ = new RecaptchaConfigAuth( - response.emailPasswordRecaptchaConfig, response.recaptchaManagedRules, - response.recaptchaKeyConfig); + if (typeof response.recaptchaConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaConfigAuth(response.recaptchaConfig); } } diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 07687c3e7e..207b3bde3f 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -92,18 +92,17 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, - emailPasswordRecaptchaConfig: { - enforcementState: 'OFF' - }, - recaptchaManagedRules: { - ruleConfigs: [{ + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { endScore: 0.2, action: 'BLOCK' - }] }, - recaptchaKeyConfig: [ { - clientType: 'WEB', - recaptchaKey: 'test-key-1' } - ], + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + } }; const clientRequestWithRecaptcha: UpdateTenantRequest = { @@ -121,15 +120,12 @@ describe('Tenant', () => { '+16505550676': '985235', }, recaptchaConfig: { - recaptchaManagedRules: { - ruleConfigs: [{ - endScore: 0.2, - action: 'BLOCK' - }] }, - emailPasswordRecaptchaConfig: { - enforcementState: 'OFF' - }, - } + managedRules: [{ + endScore: 0.2, + action: 'BLOCK' + }], + emailPasswordEnforcementState: 'AUDIT' + }, }; describe('buildServerRequest()', () => { @@ -191,72 +187,47 @@ describe('Tenant', () => { }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); - it('should throw on null ProviderRecaptchaConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); - }); - - it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + it('should throw on null emailPasswordEnforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); }); - it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { + it('should throw on invalid emailPasswordEnforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); - }); - - it('should throw on null RuleConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RuleConfig" must be a non-null object.'); - }); - - it('should throw on invalid RecaptchaManagedRules attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; + .emailPasswordEnforcementState = 'INVALID'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); - it('should throw on non-array RuleConfig attribute', () => { + it('should throw on non-array managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; + tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); }); - it('should throw on invalid RuleConfig attribute', () => { + it('should throw on invalid managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"score" is not a valid RuleConfig parameter.'); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); }); - it('should throw on invalid RuleConfig.action attribute', () => { + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); it('should throw on invalid testPhoneNumbers attribute', () => { @@ -365,72 +336,47 @@ describe('Tenant', () => { }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); - it('should throw on null ProviderRecaptchaConfig attribute', () => { + it('should throw on null emailPasswordEnforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); }); - it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + it('should throw on invalid emailPasswordEnforcementState attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); - }); - - it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); - }); - - it('should throw on null RuleConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; + .emailPasswordEnforcementState = 'INVALID'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RuleConfig" must be a non-null object.'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); - it('should throw on invalid RecaptchaManagedRules attribute', () => { + it('should throw on non-array managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; + tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); }); - it('should throw on non-array RuleConfig attribute', () => { + it('should throw on invalid managedRules attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); - }); - - it('should throw on invalid RuleConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"score" is not a valid RuleConfig parameter.'); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); }); - it('should throw on invalid RuleConfig.action attribute', () => { + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; - tenantOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + tenantOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); it('should throw on invalid testPhoneNumbers attribute', () => { @@ -534,18 +480,17 @@ describe('Tenant', () => { const serverRequestWithRecaptchaCopy: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy); - const expectedRecaptchaConfig = new RecaptchaConfigAuth( - { enforcementState: 'OFF' }, - { - ruleConfigs: [{ - endScore: 0.2, - action: 'BLOCK' - }] }, - [{ - clientType: 'WEB', - recaptchaKey: 'test-key-1' } + const expectedRecaptchaConfig = new RecaptchaConfigAuth({ + emailPasswordEnforcementState: 'AUDIT', + managedRules: [{ + endScore: 0.2, + action: 'BLOCK' + }], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } ], - ); + }); expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); }); @@ -592,11 +537,7 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), - recaptchaConfig: { - emailPasswordRecaptchaConfig: deepCopy(serverRequestWithRecaptcha.emailPasswordRecaptchaConfig), - recaptchaKeyConfig: deepCopy(serverRequestWithRecaptcha.recaptchaKeyConfig), - recaptchaManagedRules: deepCopy(serverRequestWithRecaptcha.recaptchaManagedRules), - } + recaptchaConfig: deepCopy(serverRequestWithRecaptcha.recaptchaConfig), }); }); @@ -604,9 +545,7 @@ describe('Tenant', () => { const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; - delete serverRequestCopyWithoutMfa.recaptchaKeyConfig; - delete serverRequestCopyWithoutMfa.emailPasswordRecaptchaConfig; - delete serverRequestCopyWithoutMfa.recaptchaManagedRules; + delete serverRequestCopyWithoutMfa.recaptchaConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', From 18c2e02b5b3a645b15c3119e8ada32427fc13d59 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 3 Mar 2022 11:43:48 -0800 Subject: [PATCH 07/14] remove logs --- src/auth/auth-api-request.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 2bddf2cf85..13018337da 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1896,8 +1896,6 @@ export abstract class AbstractAuthRequestHandler { requestValidator(requestData); } // Process request. - console.log(requestData); - console.log(url); const req: HttpRequestConfig = { method: apiSettings.getHttpMethod(), url, From 8298b139675b521569dce7d702b3bf0c8f172419 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Fri, 4 Mar 2022 09:25:57 -0800 Subject: [PATCH 08/14] Address PR feedback, renaming some of the variables to keep consistency --- src/auth/auth-config.ts | 2 +- src/auth/tenant.ts | 8 ++++---- test/unit/auth/tenant.spec.ts | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index e64b87d9f6..c208571eaf 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1565,7 +1565,7 @@ export interface RecaptchaConfig { recaptchaKeys?: RecaptchaKey[]; } -export class RecaptchaConfigAuth implements RecaptchaConfig { +export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; public readonly managedRules?: RecaptchaManagedRule[]; public readonly recaptchaKeys?: RecaptchaKey[]; diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 914dde0070..39d8bb8f61 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, RecaptchaConfigAuth, RecaptchaConfig + MultiFactorAuthConfig, RecaptchaAuthConfig, RecaptchaConfig } from './auth-config'; /** @@ -130,7 +130,7 @@ export class Tenant { /* * The map conatining the reCAPTCHA config. */ - private readonly recaptchaConfig_?: RecaptchaConfigAuth; + private readonly recaptchaConfig_?: RecaptchaAuthConfig; /** * Builds the corresponding server request for a TenantOptions object. * @@ -245,7 +245,7 @@ export class Tenant { } // Validate reCAPTCHAConfig type if provided. if (typeof request.recaptchaConfig !== 'undefined') { - RecaptchaConfigAuth.validate(request.recaptchaConfig); + RecaptchaAuthConfig.validate(request.recaptchaConfig); } } @@ -282,7 +282,7 @@ export class Tenant { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } if (typeof response.recaptchaConfig !== 'undefined') { - this.recaptchaConfig_ = new RecaptchaConfigAuth(response.recaptchaConfig); + this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig); } } diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 207b3bde3f..f3fa5f6059 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaConfigAuth } from '../../../src/auth/auth-config'; +import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaAuthConfig } from '../../../src/auth/auth-config'; import { TenantServerResponse } from '../../../src/auth/tenant'; import { CreateTenantRequest, UpdateTenantRequest, EmailSignInProviderConfig, Tenant, @@ -79,7 +79,7 @@ describe('Tenant', () => { }, }; - const serverRequestWithRecaptcha: TenantServerResponse = { + const serverResponseWithRecaptcha: TenantServerResponse = { name: 'projects/project1/tenants/TENANT-ID', displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, @@ -478,9 +478,9 @@ describe('Tenant', () => { it('should set readonly property recaptchaConfig', () => { const serverRequestWithRecaptchaCopy: TenantServerResponse = - deepCopy(serverRequestWithRecaptcha); + deepCopy(serverResponseWithRecaptcha); const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy); - const expectedRecaptchaConfig = new RecaptchaConfigAuth({ + const expectedRecaptchaConfig = new RecaptchaAuthConfig({ emailPasswordEnforcementState: 'AUDIT', managedRules: [{ endScore: 0.2, @@ -525,7 +525,7 @@ describe('Tenant', () => { }); describe('toJSON()', () => { - const serverRequestCopy: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); + const serverRequestCopy: TenantServerResponse = deepCopy(serverResponseWithRecaptcha); it('should return the expected object representation of a tenant', () => { expect(new Tenant(serverRequestCopy).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', @@ -537,12 +537,12 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), - recaptchaConfig: deepCopy(serverRequestWithRecaptcha.recaptchaConfig), + recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig), }); }); it('should not populate optional fields if not available', () => { - const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequestWithRecaptcha); + const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverResponseWithRecaptcha); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; delete serverRequestCopyWithoutMfa.recaptchaConfig; From 9be6dfec15f60a13beb3420f8eae77d0aa899ba6 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Mon, 7 Mar 2022 10:11:07 -0800 Subject: [PATCH 09/14] Address PR feedbacks. Major updates - Removed ManagedRuleAuth class and move the validation to recaptchaAuthConfig object. --- etc/firebase-admin.auth.api.md | 3 +- src/auth/auth-config.ts | 125 +++++++++++++++------------------ src/auth/tenant.ts | 3 + 3 files changed, 62 insertions(+), 69 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index abcd25d190..83866b5081 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -267,7 +267,7 @@ export interface ProviderIdentifier { // @public export type RecaptchaAction = 'BLOCK'; -// @public (undocumented) +// @public export interface RecaptchaConfig { emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; managedRules?: RecaptchaManagedRule[]; @@ -387,7 +387,6 @@ export interface UpdateTenantRequest { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; - // (undocumented) recaptchaConfig?: RecaptchaConfig; testPhoneNumbers?: { [phoneNumber: string]: string; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index c208571eaf..9ba7fc70c9 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1480,54 +1480,6 @@ export interface RecaptchaManagedRule { action?: RecaptchaAction; } -export class ManagedRuleAuth implements RecaptchaManagedRule{ - public readonly endScore: number; - public readonly action: RecaptchaAction; - - public static validate(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".', - ); - } - } - - constructor(response: RecaptchaManagedRule) { - if (typeof response.action === 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Invalid provider-reCAPTCHA configuration response'); - } - this.action = response.action; - if (response.endScore !== undefined) { - this.endScore = response.endScore; - } - } -} - /** * The key's platform type: only web supported now. */ @@ -1548,20 +1500,22 @@ export interface RecaptchaKey { key: string; } +/** + * The request interface for updating a reCAPTCHA Config. + */ export interface RecaptchaConfig { - /** + /** * The enforcement state of email password provider. */ emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; - - /** - * The reCAPTCHA managed rules. - */ + /** + * The reCAPTCHA managed rules. + */ managedRules?: RecaptchaManagedRule[]; - /** - * The reCAPTCHA keys. - */ + /** + * The reCAPTCHA keys. + */ recaptchaKeys?: RecaptchaKey[]; } @@ -1570,18 +1524,16 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { public readonly managedRules?: RecaptchaManagedRule[]; public readonly recaptchaKeys?: RecaptchaKey[]; - constructor(recaptchaConfig: RecaptchaConfig | undefined) { - if (recaptchaConfig?.emailPasswordEnforcementState !== undefined) { - this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; - } - if (recaptchaConfig?.managedRules !== undefined) { - this.managedRules = recaptchaConfig.managedRules; - } - if (recaptchaConfig?.recaptchaKeys !== undefined) { - this.recaptchaKeys = recaptchaConfig.recaptchaKeys; - } + 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, @@ -1633,11 +1585,50 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } // Validate each rule of the array options.managedRules.forEach((managedRule) => { - ManagedRuleAuth.validate(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, diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 39d8bb8f61..33c0b74c40 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -55,6 +55,9 @@ export interface UpdateTenantRequest { */ testPhoneNumbers?: { [phoneNumber: string]: string } | null; + /** + * The recaptcha configuration to update on the tenant. + */ recaptchaConfig?: RecaptchaConfig; } From 24dad45d49f901fe9fe74320fb3ed8b192a097a1 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:15:32 -0800 Subject: [PATCH 10/14] Update src/auth/tenant.ts Co-authored-by: Lahiru Maramba --- src/auth/tenant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 33c0b74c40..d1362fa150 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -199,7 +199,7 @@ export class Tenant { anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, - recaptchaConfig: true + recaptchaConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { From bdb0f91dab03e523cab3b8ed7e55d9bfa66a5f66 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:16:20 -0800 Subject: [PATCH 11/14] Update src/auth/auth-config.ts Co-authored-by: Lahiru Maramba --- src/auth/auth-config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 9ba7fc70c9..db10ccd975 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1562,7 +1562,8 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',) + '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.', + ); } if (options.emailPasswordEnforcementState !== 'OFF' && From b6bf4906d11fa463f2a1954b394b373f01a8e9fb Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:16:42 -0800 Subject: [PATCH 12/14] Update src/auth/tenant.ts Co-authored-by: Lahiru Maramba --- src/auth/tenant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index d1362fa150..a0ff7b242d 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -73,7 +73,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; - recaptchaConfig? : RecaptchaConfig; + recaptchaConfig?: RecaptchaConfig; } /** The tenant server response interface. */ From e0c142754719b085e246a1ca6f094b0422368027 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:16:46 -0800 Subject: [PATCH 13/14] Update src/auth/auth-config.ts Co-authored-by: Lahiru Maramba --- src/auth/auth-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index db10ccd975..ac370f6421 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1558,7 +1558,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } // Validation - if (options.emailPasswordEnforcementState !== undefined){ + if (typeof options.emailPasswordEnforcementState !== undefined) { if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, From ee241f77c5e9518ffeae78e08ca300caabe4a907 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Mon, 7 Mar 2022 13:22:11 -0800 Subject: [PATCH 14/14] fix lint --- src/auth/auth-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index ac370f6421..aecd686201 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1563,7 +1563,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.', - ); + ); } if (options.emailPasswordEnforcementState !== 'OFF' &&