From 6f2b0dda348ca375e730c66710edc677cd81975a Mon Sep 17 00:00:00 2001 From: "Nhien (Ricky) Lam" <62775270+NhienLam@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:18:23 -0700 Subject: [PATCH] Get reCAPTCHA Enterprise enforcement state of a provider (#7685) * Get recaptcha enforcement state for a provider * Fix lint * Format * Resolve review comments * Merge master branch into recaptcha-provider * Fix lint * Add changeset * Rename recaptchaEnforcementStateList to recaptchaEnforcementState * Modify changesset --- .changeset/khaki-apricots-doubt.md | 6 ++ .../auth/src/api/authentication/recaptcha.ts | 4 +- packages/auth/src/api/index.ts | 27 +++++++++ packages/auth/src/core/auth/auth_impl.test.ts | 14 ++++- .../recaptcha/recaptcha.test.ts | 42 +++++++++++++- .../platform_browser/recaptcha/recaptcha.ts | 55 ++++++++++++++++--- .../recaptcha_enterprise_verifier.ts | 11 +++- 7 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 .changeset/khaki-apricots-doubt.md diff --git a/.changeset/khaki-apricots-doubt.md b/.changeset/khaki-apricots-doubt.md new file mode 100644 index 00000000000..dc3cf14a0ee --- /dev/null +++ b/.changeset/khaki-apricots-doubt.md @@ -0,0 +1,6 @@ +--- +'@firebase/auth': patch +--- + +Create getProviderEnforcementState method to get reCAPTCHA Enterprise enforcement state of a provider. +This is an internal code change preparing for future features. diff --git a/packages/auth/src/api/authentication/recaptcha.ts b/packages/auth/src/api/authentication/recaptcha.ts index ab3633f6214..c676a6991e5 100644 --- a/packages/auth/src/api/authentication/recaptcha.ts +++ b/packages/auth/src/api/authentication/recaptcha.ts @@ -48,14 +48,14 @@ interface GetRecaptchaConfigRequest { version?: RecaptchaVersion; } -interface RecaptchaEnforcementState { +export interface RecaptchaEnforcementProviderState { provider: string; enforcementState: string; } export interface GetRecaptchaConfigResponse { recaptchaKey: string; - recaptchaEnforcementState: RecaptchaEnforcementState[]; + recaptchaEnforcementState: RecaptchaEnforcementProviderState[]; } export async function getRecaptchaConfig( diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index 4a17173099f..27acea42895 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -87,6 +87,18 @@ export const enum RecaptchaActionName { SIGN_UP_PASSWORD = 'signUpPassword' } +export const enum EnforcementState { + ENFORCE = 'ENFORCE', + AUDIT = 'AUDIT', + OFF = 'OFF', + ENFORCEMENT_STATE_UNSPECIFIED = 'ENFORCEMENT_STATE_UNSPECIFIED' +} + +// Providers that have reCAPTCHA Enterprise support. +export const enum RecaptchaProvider { + EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER' +} + export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000); export function _addTidIfNecessary( @@ -245,6 +257,21 @@ export function _getFinalTarget( return _emulatorUrl(auth.config as ConfigInternal, base); } +export function _parseEnforcementState( + enforcementStateStr: string +): EnforcementState { + switch (enforcementStateStr) { + case 'ENFORCE': + return EnforcementState.ENFORCE; + case 'AUDIT': + return EnforcementState.AUDIT; + case 'OFF': + return EnforcementState.OFF; + default: + return EnforcementState.ENFORCEMENT_STATE_UNSPECIFIED; + } +} + class NetworkTimeout { // Node timers and browser timers are fundamentally incompatible, but we // don't care about the value here diff --git a/packages/auth/src/core/auth/auth_impl.test.ts b/packages/auth/src/core/auth/auth_impl.test.ts index baa9abff1d7..3aa8623d40e 100644 --- a/packages/auth/src/core/auth/auth_impl.test.ts +++ b/packages/auth/src/core/auth/auth_impl.test.ts @@ -710,11 +710,21 @@ describe('core/auth/auth_impl', () => { ] }; const cachedRecaptchaConfigEnforce = { - emailPasswordEnabled: true, + recaptchaEnforcementState: [ + { + 'enforcementState': 'ENFORCE', + 'provider': 'EMAIL_PASSWORD_PROVIDER' + } + ], siteKey: 'site-key' }; const cachedRecaptchaConfigOFF = { - emailPasswordEnabled: false, + recaptchaEnforcementState: [ + { + 'enforcementState': 'OFF', + 'provider': 'EMAIL_PASSWORD_PROVIDER' + } + ], siteKey: 'site-key' }; diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts index e0ff090d8dd..42a758840f6 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts @@ -27,7 +27,9 @@ import { MockGreCAPTCHA } from './recaptcha_mock'; -import { isV2, isEnterprise } from './recaptcha'; +import { isV2, isEnterprise, RecaptchaConfig } from './recaptcha'; +import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha'; +import { EnforcementState } from '../../api/index'; use(chaiAsPromised); use(sinonChai); @@ -37,6 +39,16 @@ describe('platform_browser/recaptcha/recaptcha', () => { let recaptchaV2: MockReCaptcha; let recaptchaV3: MockGreCAPTCHA; let recaptchaEnterprise: MockGreCAPTCHATopLevel; + let recaptchaConfig: RecaptchaConfig; + + const TEST_SITE_KEY = 'test-site-key'; + + const GET_RECAPTCHA_CONFIG_RESPONSE: GetRecaptchaConfigResponse = { + recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY, + recaptchaEnforcementState: [ + { provider: 'EMAIL_PASSWORD_PROVIDER', enforcementState: 'ENFORCE' } + ] + }; context('#verify', () => { beforeEach(async () => { @@ -60,4 +72,32 @@ describe('platform_browser/recaptcha/recaptcha', () => { expect(isEnterprise(recaptchaEnterprise)).to.be.true; }); }); + + context('#RecaptchaConfig', () => { + beforeEach(async () => { + recaptchaConfig = new RecaptchaConfig(GET_RECAPTCHA_CONFIG_RESPONSE); + }); + + it('should construct the recaptcha config from the backend response', () => { + expect(recaptchaConfig.siteKey).to.eq(TEST_SITE_KEY); + expect(recaptchaConfig.recaptchaEnforcementState[0]).to.eql({ + provider: 'EMAIL_PASSWORD_PROVIDER', + enforcementState: 'ENFORCE' + }); + }); + + it('#getProviderEnforcementState should return the correct enforcement state of the provider', () => { + expect( + recaptchaConfig.getProviderEnforcementState('EMAIL_PASSWORD_PROVIDER') + ).to.eq(EnforcementState.ENFORCE); + expect(recaptchaConfig.getProviderEnforcementState('invalid-provider')).to + .be.null; + }); + + it('#isProviderEnabled should return the enablement state of the provider', () => { + expect(recaptchaConfig.isProviderEnabled('EMAIL_PASSWORD_PROVIDER')).to.be + .true; + expect(recaptchaConfig.isProviderEnabled('invalid-provider')).to.be.false; + }); + }); }); diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha.ts index ab1b73e35f7..bb1b79895c0 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha.ts @@ -16,7 +16,11 @@ */ import { RecaptchaParameters } from '../../model/public_types'; -import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha'; +import { + GetRecaptchaConfigResponse, + RecaptchaEnforcementProviderState +} from '../../api/authentication/recaptcha'; +import { EnforcementState, _parseEnforcementState } from '../../api/index'; // reCAPTCHA v2 interface export interface Recaptcha { @@ -78,9 +82,9 @@ export class RecaptchaConfig { siteKey: string = ''; /** - * The reCAPTCHA enablement status of the {@link EmailAuthProvider} for the current tenant. + * The list of providers and their enablement status for reCAPTCHA Enterprise. */ - emailPasswordEnabled: boolean = false; + recaptchaEnforcementState: RecaptchaEnforcementProviderState[] = []; constructor(response: GetRecaptchaConfigResponse) { if (response.recaptchaKey === undefined) { @@ -88,10 +92,47 @@ export class RecaptchaConfig { } // Example response.recaptchaKey: "projects/proj123/keys/sitekey123" this.siteKey = response.recaptchaKey.split('/')[3]; - this.emailPasswordEnabled = response.recaptchaEnforcementState.some( - enforcementState => - enforcementState.provider === 'EMAIL_PASSWORD_PROVIDER' && - enforcementState.enforcementState !== 'OFF' + this.recaptchaEnforcementState = response.recaptchaEnforcementState; + } + + /** + * Returns the reCAPTCHA Enterprise enforcement state for the given provider. + * + * @param providerStr - The provider whose enforcement state is to be returned. + * @returns The reCAPTCHA Enterprise enforcement state for the given provider. + */ + getProviderEnforcementState(providerStr: string): EnforcementState | null { + if ( + !this.recaptchaEnforcementState || + this.recaptchaEnforcementState.length === 0 + ) { + return null; + } + + for (const recaptchaEnforcementState of this.recaptchaEnforcementState) { + if ( + recaptchaEnforcementState.provider && + recaptchaEnforcementState.provider === providerStr + ) { + return _parseEnforcementState( + recaptchaEnforcementState.enforcementState + ); + } + } + return null; + } + + /** + * Returns true if the reCAPTCHA Enterprise enforcement state for the provider is set to ENFORCE or AUDIT. + * + * @param providerStr - The provider whose enablement state is to be returned. + * @returns Whether or not reCAPTCHA Enterprise protection is enabled for the given provider. + */ + isProviderEnabled(providerStr: string): boolean { + return ( + this.getProviderEnforcementState(providerStr) === + EnforcementState.ENFORCE || + this.getProviderEnforcementState(providerStr) === EnforcementState.AUDIT ); } } diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts index 63ec91fceae..8d188032e36 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts @@ -21,7 +21,8 @@ import { getRecaptchaConfig } from '../../api/authentication/recaptcha'; import { RecaptchaClientType, RecaptchaVersion, - RecaptchaActionName + RecaptchaActionName, + RecaptchaProvider } from '../../api'; import { Auth } from '../../model/public_types'; @@ -187,7 +188,11 @@ export async function handleRecaptchaFlow( actionName: RecaptchaActionName, actionMethod: ActionMethod ): Promise { - if (authInstance._getRecaptchaConfig()?.emailPasswordEnabled) { + if ( + authInstance + ._getRecaptchaConfig() + ?.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER) + ) { const requestWithRecaptcha = await injectRecaptchaFields( authInstance, request, @@ -230,7 +235,7 @@ export async function _initializeRecaptchaConfig(auth: Auth): Promise { authInternal._tenantRecaptchaConfigs[authInternal.tenantId] = config; } - if (config.emailPasswordEnabled) { + if (config.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)) { const verifier = new RecaptchaEnterpriseVerifier(authInternal); void verifier.verify(); }