Skip to content

Commit 68927ce

Browse files
authored
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
1 parent 12f2559 commit 68927ce

File tree

7 files changed

+144
-15
lines changed

7 files changed

+144
-15
lines changed

.changeset/khaki-apricots-doubt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/auth': patch
3+
---
4+
5+
Create getProviderEnforcementState method to get reCAPTCHA Enterprise enforcement state of a provider.
6+
This is an internal code change preparing for future features.

packages/auth/src/api/authentication/recaptcha.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ interface GetRecaptchaConfigRequest {
4848
version?: RecaptchaVersion;
4949
}
5050

51-
interface RecaptchaEnforcementState {
51+
export interface RecaptchaEnforcementProviderState {
5252
provider: string;
5353
enforcementState: string;
5454
}
5555

5656
export interface GetRecaptchaConfigResponse {
5757
recaptchaKey: string;
58-
recaptchaEnforcementState: RecaptchaEnforcementState[];
58+
recaptchaEnforcementState: RecaptchaEnforcementProviderState[];
5959
}
6060

6161
export async function getRecaptchaConfig(

packages/auth/src/api/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ export const enum RecaptchaActionName {
8787
SIGN_UP_PASSWORD = 'signUpPassword'
8888
}
8989

90+
export const enum EnforcementState {
91+
ENFORCE = 'ENFORCE',
92+
AUDIT = 'AUDIT',
93+
OFF = 'OFF',
94+
ENFORCEMENT_STATE_UNSPECIFIED = 'ENFORCEMENT_STATE_UNSPECIFIED'
95+
}
96+
97+
// Providers that have reCAPTCHA Enterprise support.
98+
export const enum RecaptchaProvider {
99+
EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER'
100+
}
101+
90102
export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000);
91103

92104
export function _addTidIfNecessary<T extends { tenantId?: string }>(
@@ -245,6 +257,21 @@ export function _getFinalTarget(
245257
return _emulatorUrl(auth.config as ConfigInternal, base);
246258
}
247259

260+
export function _parseEnforcementState(
261+
enforcementStateStr: string
262+
): EnforcementState {
263+
switch (enforcementStateStr) {
264+
case 'ENFORCE':
265+
return EnforcementState.ENFORCE;
266+
case 'AUDIT':
267+
return EnforcementState.AUDIT;
268+
case 'OFF':
269+
return EnforcementState.OFF;
270+
default:
271+
return EnforcementState.ENFORCEMENT_STATE_UNSPECIFIED;
272+
}
273+
}
274+
248275
class NetworkTimeout<T> {
249276
// Node timers and browser timers are fundamentally incompatible, but we
250277
// don't care about the value here

packages/auth/src/core/auth/auth_impl.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,11 +710,21 @@ describe('core/auth/auth_impl', () => {
710710
]
711711
};
712712
const cachedRecaptchaConfigEnforce = {
713-
emailPasswordEnabled: true,
713+
recaptchaEnforcementState: [
714+
{
715+
'enforcementState': 'ENFORCE',
716+
'provider': 'EMAIL_PASSWORD_PROVIDER'
717+
}
718+
],
714719
siteKey: 'site-key'
715720
};
716721
const cachedRecaptchaConfigOFF = {
717-
emailPasswordEnabled: false,
722+
recaptchaEnforcementState: [
723+
{
724+
'enforcementState': 'OFF',
725+
'provider': 'EMAIL_PASSWORD_PROVIDER'
726+
}
727+
],
718728
siteKey: 'site-key'
719729
};
720730

packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {
2727
MockGreCAPTCHA
2828
} from './recaptcha_mock';
2929

30-
import { isV2, isEnterprise } from './recaptcha';
30+
import { isV2, isEnterprise, RecaptchaConfig } from './recaptcha';
31+
import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha';
32+
import { EnforcementState } from '../../api/index';
3133

3234
use(chaiAsPromised);
3335
use(sinonChai);
@@ -37,6 +39,16 @@ describe('platform_browser/recaptcha/recaptcha', () => {
3739
let recaptchaV2: MockReCaptcha;
3840
let recaptchaV3: MockGreCAPTCHA;
3941
let recaptchaEnterprise: MockGreCAPTCHATopLevel;
42+
let recaptchaConfig: RecaptchaConfig;
43+
44+
const TEST_SITE_KEY = 'test-site-key';
45+
46+
const GET_RECAPTCHA_CONFIG_RESPONSE: GetRecaptchaConfigResponse = {
47+
recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY,
48+
recaptchaEnforcementState: [
49+
{ provider: 'EMAIL_PASSWORD_PROVIDER', enforcementState: 'ENFORCE' }
50+
]
51+
};
4052

4153
context('#verify', () => {
4254
beforeEach(async () => {
@@ -60,4 +72,32 @@ describe('platform_browser/recaptcha/recaptcha', () => {
6072
expect(isEnterprise(recaptchaEnterprise)).to.be.true;
6173
});
6274
});
75+
76+
context('#RecaptchaConfig', () => {
77+
beforeEach(async () => {
78+
recaptchaConfig = new RecaptchaConfig(GET_RECAPTCHA_CONFIG_RESPONSE);
79+
});
80+
81+
it('should construct the recaptcha config from the backend response', () => {
82+
expect(recaptchaConfig.siteKey).to.eq(TEST_SITE_KEY);
83+
expect(recaptchaConfig.recaptchaEnforcementState[0]).to.eql({
84+
provider: 'EMAIL_PASSWORD_PROVIDER',
85+
enforcementState: 'ENFORCE'
86+
});
87+
});
88+
89+
it('#getProviderEnforcementState should return the correct enforcement state of the provider', () => {
90+
expect(
91+
recaptchaConfig.getProviderEnforcementState('EMAIL_PASSWORD_PROVIDER')
92+
).to.eq(EnforcementState.ENFORCE);
93+
expect(recaptchaConfig.getProviderEnforcementState('invalid-provider')).to
94+
.be.null;
95+
});
96+
97+
it('#isProviderEnabled should return the enablement state of the provider', () => {
98+
expect(recaptchaConfig.isProviderEnabled('EMAIL_PASSWORD_PROVIDER')).to.be
99+
.true;
100+
expect(recaptchaConfig.isProviderEnabled('invalid-provider')).to.be.false;
101+
});
102+
});
63103
});

packages/auth/src/platform_browser/recaptcha/recaptcha.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
*/
1717

1818
import { RecaptchaParameters } from '../../model/public_types';
19-
import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha';
19+
import {
20+
GetRecaptchaConfigResponse,
21+
RecaptchaEnforcementProviderState
22+
} from '../../api/authentication/recaptcha';
23+
import { EnforcementState, _parseEnforcementState } from '../../api/index';
2024

2125
// reCAPTCHA v2 interface
2226
export interface Recaptcha {
@@ -78,20 +82,57 @@ export class RecaptchaConfig {
7882
siteKey: string = '';
7983

8084
/**
81-
* The reCAPTCHA enablement status of the {@link EmailAuthProvider} for the current tenant.
85+
* The list of providers and their enablement status for reCAPTCHA Enterprise.
8286
*/
83-
emailPasswordEnabled: boolean = false;
87+
recaptchaEnforcementState: RecaptchaEnforcementProviderState[] = [];
8488

8589
constructor(response: GetRecaptchaConfigResponse) {
8690
if (response.recaptchaKey === undefined) {
8791
throw new Error('recaptchaKey undefined');
8892
}
8993
// Example response.recaptchaKey: "projects/proj123/keys/sitekey123"
9094
this.siteKey = response.recaptchaKey.split('/')[3];
91-
this.emailPasswordEnabled = response.recaptchaEnforcementState.some(
92-
enforcementState =>
93-
enforcementState.provider === 'EMAIL_PASSWORD_PROVIDER' &&
94-
enforcementState.enforcementState !== 'OFF'
95+
this.recaptchaEnforcementState = response.recaptchaEnforcementState;
96+
}
97+
98+
/**
99+
* Returns the reCAPTCHA Enterprise enforcement state for the given provider.
100+
*
101+
* @param providerStr - The provider whose enforcement state is to be returned.
102+
* @returns The reCAPTCHA Enterprise enforcement state for the given provider.
103+
*/
104+
getProviderEnforcementState(providerStr: string): EnforcementState | null {
105+
if (
106+
!this.recaptchaEnforcementState ||
107+
this.recaptchaEnforcementState.length === 0
108+
) {
109+
return null;
110+
}
111+
112+
for (const recaptchaEnforcementState of this.recaptchaEnforcementState) {
113+
if (
114+
recaptchaEnforcementState.provider &&
115+
recaptchaEnforcementState.provider === providerStr
116+
) {
117+
return _parseEnforcementState(
118+
recaptchaEnforcementState.enforcementState
119+
);
120+
}
121+
}
122+
return null;
123+
}
124+
125+
/**
126+
* Returns true if the reCAPTCHA Enterprise enforcement state for the provider is set to ENFORCE or AUDIT.
127+
*
128+
* @param providerStr - The provider whose enablement state is to be returned.
129+
* @returns Whether or not reCAPTCHA Enterprise protection is enabled for the given provider.
130+
*/
131+
isProviderEnabled(providerStr: string): boolean {
132+
return (
133+
this.getProviderEnforcementState(providerStr) ===
134+
EnforcementState.ENFORCE ||
135+
this.getProviderEnforcementState(providerStr) === EnforcementState.AUDIT
95136
);
96137
}
97138
}

packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { getRecaptchaConfig } from '../../api/authentication/recaptcha';
2121
import {
2222
RecaptchaClientType,
2323
RecaptchaVersion,
24-
RecaptchaActionName
24+
RecaptchaActionName,
25+
RecaptchaProvider
2526
} from '../../api';
2627

2728
import { Auth } from '../../model/public_types';
@@ -187,7 +188,11 @@ export async function handleRecaptchaFlow<TRequest, TResponse>(
187188
actionName: RecaptchaActionName,
188189
actionMethod: ActionMethod<TRequest, TResponse>
189190
): Promise<TResponse> {
190-
if (authInstance._getRecaptchaConfig()?.emailPasswordEnabled) {
191+
if (
192+
authInstance
193+
._getRecaptchaConfig()
194+
?.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)
195+
) {
191196
const requestWithRecaptcha = await injectRecaptchaFields(
192197
authInstance,
193198
request,
@@ -230,7 +235,7 @@ export async function _initializeRecaptchaConfig(auth: Auth): Promise<void> {
230235
authInternal._tenantRecaptchaConfigs[authInternal.tenantId] = config;
231236
}
232237

233-
if (config.emailPasswordEnabled) {
238+
if (config.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)) {
234239
const verifier = new RecaptchaEnterpriseVerifier(authInternal);
235240
void verifier.verify();
236241
}

0 commit comments

Comments
 (0)