Skip to content

Commit

Permalink
Get reCAPTCHA Enterprise enforcement state of a provider (#7685)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
NhienLam authored Oct 18, 2023
1 parent 12f2559 commit 68927ce
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 15 deletions.
6 changes: 6 additions & 0 deletions .changeset/khaki-apricots-doubt.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/auth/src/api/authentication/recaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends { tenantId?: string }>(
Expand Down Expand Up @@ -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<T> {
// Node timers and browser timers are fundamentally incompatible, but we
// don't care about the value here
Expand Down
14 changes: 12 additions & 2 deletions packages/auth/src/core/auth/auth_impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
};

Expand Down
42 changes: 41 additions & 1 deletion packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () => {
Expand All @@ -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;
});
});
});
55 changes: 48 additions & 7 deletions packages/auth/src/platform_browser/recaptcha/recaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -78,20 +82,57 @@ 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) {
throw new Error('recaptchaKey undefined');
}
// 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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { getRecaptchaConfig } from '../../api/authentication/recaptcha';
import {
RecaptchaClientType,
RecaptchaVersion,
RecaptchaActionName
RecaptchaActionName,
RecaptchaProvider
} from '../../api';

import { Auth } from '../../model/public_types';
Expand Down Expand Up @@ -187,7 +188,11 @@ export async function handleRecaptchaFlow<TRequest, TResponse>(
actionName: RecaptchaActionName,
actionMethod: ActionMethod<TRequest, TResponse>
): Promise<TResponse> {
if (authInstance._getRecaptchaConfig()?.emailPasswordEnabled) {
if (
authInstance
._getRecaptchaConfig()
?.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)
) {
const requestWithRecaptcha = await injectRecaptchaFields(
authInstance,
request,
Expand Down Expand Up @@ -230,7 +235,7 @@ export async function _initializeRecaptchaConfig(auth: Auth): Promise<void> {
authInternal._tenantRecaptchaConfigs[authInternal.tenantId] = config;
}

if (config.emailPasswordEnabled) {
if (config.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)) {
const verifier = new RecaptchaEnterpriseVerifier(authInternal);
void verifier.verify();
}
Expand Down

0 comments on commit 68927ce

Please sign in to comment.