Skip to content

Get reCAPTCHA enforcement state of a provider #7685

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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