Skip to content

Commit dab6702

Browse files
committed
Get recaptcha enforcement state for a provider
1 parent 12f2559 commit dab6702

File tree

9 files changed

+156
-17
lines changed

9 files changed

+156
-17
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ interface GetRecaptchaConfigRequest {
4848
version?: RecaptchaVersion;
4949
}
5050

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

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 has 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+
recaptchaEnforcementStateList: [
714+
{
715+
'enforcementState': 'ENFORCE',
716+
'provider': 'EMAIL_PASSWORD_PROVIDER'
717+
}
718+
],
714719
siteKey: 'site-key'
715720
};
716721
const cachedRecaptchaConfigOFF = {
717-
emailPasswordEnabled: false,
722+
recaptchaEnforcementStateList: [
723+
{
724+
'enforcementState': 'OFF',
725+
'provider': 'EMAIL_PASSWORD_PROVIDER'
726+
}
727+
],
718728
siteKey: 'site-key'
719729
};
720730

packages/auth/src/core/credentials/email.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import { AuthErrorCode } from '../errors';
3232
import { _fail } from '../util/assert';
3333
import { AuthCredential } from './auth_credential';
3434
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
35-
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
35+
import {
36+
RecaptchaActionName,
37+
RecaptchaClientType,
38+
RecaptchaProvider
39+
} from '../../api';
3640
/**
3741
* Interface that represents the credentials returned by {@link EmailAuthProvider} for
3842
* {@link ProviderId}.PASSWORD

packages/auth/src/core/strategies/email_and_password.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ import { getModularInstance } from '@firebase/util';
3838
import { OperationType } from '../../model/enums';
3939
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
4040
import { IdTokenResponse } from '../../model/id_token';
41-
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
41+
import {
42+
RecaptchaActionName,
43+
RecaptchaClientType,
44+
RecaptchaProvider
45+
} from '../../api';
4246

4347
/**
4448
* Updates the password policy cached in the {@link Auth} instance if a policy is already

packages/auth/src/core/strategies/email_link.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import { _assert } from '../util/assert';
3333
import { getModularInstance } from '@firebase/util';
3434
import { _castAuth } from '../auth/auth_impl';
3535
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
36-
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
36+
import {
37+
RecaptchaActionName,
38+
RecaptchaClientType,
39+
RecaptchaProvider
40+
} from '../../api';
3741

3842
/**
3943
* Sends a sign-in email link to the user with the specified email.

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.recaptchaEnforcementStateList[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: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616
*/
1717

1818
import { RecaptchaParameters } from '../../model/public_types';
19-
import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha';
19+
import {
20+
GetRecaptchaConfigResponse,
21+
RecaptchaEnforcementState
22+
} from '../../api/authentication/recaptcha';
23+
import {
24+
EnforcementState,
25+
RecaptchaProvider,
26+
_parseEnforcementState
27+
} from '../../api/index';
2028

2129
// reCAPTCHA v2 interface
2230
export interface Recaptcha {
@@ -78,20 +86,61 @@ export class RecaptchaConfig {
7886
siteKey: string = '';
7987

8088
/**
81-
* The reCAPTCHA enablement status of the {@link EmailAuthProvider} for the current tenant.
89+
* The list of providers and their enablement status for reCAPTCHA.
8290
*/
83-
emailPasswordEnabled: boolean = false;
91+
recaptchaEnforcementStateList: RecaptchaEnforcementState[] = [];
8492

8593
constructor(response: GetRecaptchaConfigResponse) {
8694
if (response.recaptchaKey === undefined) {
8795
throw new Error('recaptchaKey undefined');
8896
}
8997
// Example response.recaptchaKey: "projects/proj123/keys/sitekey123"
9098
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-
);
99+
this.recaptchaEnforcementStateList = response.recaptchaEnforcementState;
100+
}
101+
102+
/**
103+
* Returns the reCAPTCHA enforcement state for the given provider.
104+
*
105+
* @param providerStr - The provider whose enforcement state is to be returned.
106+
* @returns The reCAPTCHA enforcement state for the given provider.
107+
*/
108+
getProviderEnforcementState(providerStr: string): EnforcementState | null {
109+
if (
110+
!this.recaptchaEnforcementStateList ||
111+
this.recaptchaEnforcementStateList.length === 0
112+
) {
113+
return null;
114+
}
115+
116+
for (const recaptchaEnforcementState of this
117+
.recaptchaEnforcementStateList) {
118+
if (
119+
recaptchaEnforcementState.provider &&
120+
recaptchaEnforcementState.provider === providerStr
121+
) {
122+
return _parseEnforcementState(
123+
recaptchaEnforcementState.enforcementState
124+
);
125+
}
126+
}
127+
return null;
128+
}
129+
130+
/**
131+
* Returns true if the reCAPTCHA enforcement state for the provider is set to ENFORCE or AUDIT.
132+
*
133+
* @param providerStr - The provider whose enablement state is to be returned.
134+
* @returns Whether or not reCAPTCHA protection is enabled for the given provider.
135+
*/
136+
isProviderEnabled(providerStr: string): boolean {
137+
if (
138+
this.getProviderEnforcementState(providerStr) ===
139+
EnforcementState.ENFORCE ||
140+
this.getProviderEnforcementState(providerStr) === EnforcementState.AUDIT
141+
) {
142+
return true;
143+
}
144+
return false;
96145
}
97146
}

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

Lines changed: 3 additions & 2 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';
@@ -230,7 +231,7 @@ export async function _initializeRecaptchaConfig(auth: Auth): Promise<void> {
230231
authInternal._tenantRecaptchaConfigs[authInternal.tenantId] = config;
231232
}
232233

233-
if (config.emailPasswordEnabled) {
234+
if (config.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)) {
234235
const verifier = new RecaptchaEnterpriseVerifier(authInternal);
235236
void verifier.verify();
236237
}

0 commit comments

Comments
 (0)