From 3bfd025b0e30626eae27c6b40cd0974526eac6ac Mon Sep 17 00:00:00 2001 From: "Nhien (Ricky) Lam" <62775270+NhienLam@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:26:54 -0800 Subject: [PATCH] Update injectRecaptchaFields to inject recaptcha enterprise fields into phone API requests (#7786) * Update injectRecaptchaFields to inject recaptcha fields into phone API requests * Fix lint * Rename captchaResp and fakeToken params * Format --- .../src/api/account_management/mfa.test.ts | 12 +- .../auth/src/api/account_management/mfa.ts | 9 +- .../auth/src/api/authentication/mfa.test.ts | 12 +- packages/auth/src/api/authentication/mfa.ts | 9 +- .../auth/src/api/authentication/sms.test.ts | 12 +- packages/auth/src/api/authentication/sms.ts | 9 +- packages/auth/src/api/index.ts | 8 +- .../recaptcha/recaptcha.test.ts | 100 +++++++-- .../platform_browser/recaptcha/recaptcha.ts | 19 +- .../recaptcha_enterprise_verifier.test.ts | 191 ++++++++++++++++-- .../recaptcha_enterprise_verifier.ts | 61 +++++- 11 files changed, 391 insertions(+), 51 deletions(-) diff --git a/packages/auth/src/api/account_management/mfa.test.ts b/packages/auth/src/api/account_management/mfa.test.ts index 9f036471087..152f1fb53d3 100644 --- a/packages/auth/src/api/account_management/mfa.test.ts +++ b/packages/auth/src/api/account_management/mfa.test.ts @@ -20,7 +20,12 @@ import chaiAsPromised from 'chai-as-promised'; import { FirebaseError } from '@firebase/util'; -import { Endpoint, HttpHeader } from '../'; +import { + Endpoint, + HttpHeader, + RecaptchaClientType, + RecaptchaVersion +} from '../'; import { mockEndpoint } from '../../../test/helpers/api/helper'; import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; import * as mockFetch from '../../../test/helpers/mock_fetch'; @@ -40,7 +45,10 @@ describe('api/account_management/startEnrollPhoneMfa', () => { idToken: 'id-token', phoneEnrollmentInfo: { phoneNumber: 'phone-number', - recaptchaToken: 'captcha-token' + recaptchaToken: 'captcha-token', + captchaResponse: 'captcha-response', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE } }; diff --git a/packages/auth/src/api/account_management/mfa.ts b/packages/auth/src/api/account_management/mfa.ts index db9a4120d3d..f43217e8abf 100644 --- a/packages/auth/src/api/account_management/mfa.ts +++ b/packages/auth/src/api/account_management/mfa.ts @@ -18,6 +18,8 @@ import { Endpoint, HttpMethod, + RecaptchaClientType, + RecaptchaVersion, _addTidIfNecessary, _performApiRequest } from '../index'; @@ -55,7 +57,12 @@ export interface StartPhoneMfaEnrollmentRequest { idToken: string; phoneEnrollmentInfo: { phoneNumber: string; - recaptchaToken: string; + // reCAPTCHA v2 token + recaptchaToken?: string; + // reCAPTCHA Enterprise token + captchaResponse?: string; + clientType?: RecaptchaClientType; + recaptchaVersion?: RecaptchaVersion; }; tenantId?: string; } diff --git a/packages/auth/src/api/authentication/mfa.test.ts b/packages/auth/src/api/authentication/mfa.test.ts index 2289f49ca0c..b732a3e83b8 100644 --- a/packages/auth/src/api/authentication/mfa.test.ts +++ b/packages/auth/src/api/authentication/mfa.test.ts @@ -20,7 +20,12 @@ import chaiAsPromised from 'chai-as-promised'; import { FirebaseError } from '@firebase/util'; -import { Endpoint, HttpHeader } from '../'; +import { + Endpoint, + HttpHeader, + RecaptchaClientType, + RecaptchaVersion +} from '../'; import { mockEndpoint } from '../../../test/helpers/api/helper'; import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; import * as mockFetch from '../../../test/helpers/mock_fetch'; @@ -34,7 +39,10 @@ describe('api/authentication/startSignInPhoneMfa', () => { mfaPendingCredential: 'my-creds', mfaEnrollmentId: 'my-enrollment-id', phoneSignInInfo: { - recaptchaToken: 'catpcha-token' + recaptchaToken: 'catpcha-token', + captchaResponse: 'captcha-response', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE } }; diff --git a/packages/auth/src/api/authentication/mfa.ts b/packages/auth/src/api/authentication/mfa.ts index 0ad85a7ef82..c6dc6854e98 100644 --- a/packages/auth/src/api/authentication/mfa.ts +++ b/packages/auth/src/api/authentication/mfa.ts @@ -19,6 +19,8 @@ import { _performApiRequest, Endpoint, HttpMethod, + RecaptchaClientType, + RecaptchaVersion, _addTidIfNecessary } from '../index'; import { Auth } from '../../model/public_types'; @@ -47,7 +49,12 @@ export interface StartPhoneMfaSignInRequest { mfaPendingCredential: string; mfaEnrollmentId: string; phoneSignInInfo: { - recaptchaToken: string; + // reCAPTCHA v2 token + recaptchaToken?: string; + // reCAPTCHA Enterprise token + captchaResponse?: string; + clientType?: RecaptchaClientType; + recaptchaVersion?: RecaptchaVersion; }; tenantId?: string; } diff --git a/packages/auth/src/api/authentication/sms.test.ts b/packages/auth/src/api/authentication/sms.test.ts index 420797b1c97..d72e7973077 100644 --- a/packages/auth/src/api/authentication/sms.test.ts +++ b/packages/auth/src/api/authentication/sms.test.ts @@ -21,7 +21,12 @@ import chaiAsPromised from 'chai-as-promised'; import { ProviderId } from '../../model/enums'; import { FirebaseError } from '@firebase/util'; -import { Endpoint, HttpHeader } from '../'; +import { + Endpoint, + HttpHeader, + RecaptchaClientType, + RecaptchaVersion +} from '../'; import { mockEndpoint } from '../../../test/helpers/api/helper'; import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; import * as mockFetch from '../../../test/helpers/mock_fetch'; @@ -38,7 +43,10 @@ use(chaiAsPromised); describe('api/authentication/sendPhoneVerificationCode', () => { const request = { phoneNumber: '123456789', - recaptchaToken: 'captchad' + recaptchaToken: 'captchad', + captchaResponse: 'captcha-response', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE }; let auth: TestAuth; diff --git a/packages/auth/src/api/authentication/sms.ts b/packages/auth/src/api/authentication/sms.ts index 93a8a2e5abf..26db0634288 100644 --- a/packages/auth/src/api/authentication/sms.ts +++ b/packages/auth/src/api/authentication/sms.ts @@ -18,6 +18,8 @@ import { Endpoint, HttpMethod, + RecaptchaClientType, + RecaptchaVersion, _addTidIfNecessary, _makeTaggedError, _performApiRequest, @@ -30,8 +32,13 @@ import { Auth } from '../../model/public_types'; export interface SendPhoneVerificationCodeRequest { phoneNumber: string; - recaptchaToken: string; + // reCAPTCHA v2 token + recaptchaToken?: string; tenantId?: string; + // reCAPTCHA Enterprise token + captchaResponse?: string; + clientType?: RecaptchaClientType; + recaptchaVersion?: RecaptchaVersion; } export interface SendPhoneVerificationCodeResponse { diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index 81ecf5a356a..f61ba74d1c6 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -86,7 +86,10 @@ export const enum RecaptchaVersion { export const enum RecaptchaActionName { SIGN_IN_WITH_PASSWORD = 'signInWithPassword', GET_OOB_CODE = 'getOobCode', - SIGN_UP_PASSWORD = 'signUpPassword' + SIGN_UP_PASSWORD = 'signUpPassword', + SEND_VERIFICATION_CODE = 'sendVerificationCode', + MFA_SMS_ENROLLMENT = 'mfaSmsEnrollment', + MFA_SMS_SIGNIN = 'mfaSmsSignin' } export const enum EnforcementState { @@ -98,7 +101,8 @@ export const enum EnforcementState { // Providers that have reCAPTCHA Enterprise support. export const enum RecaptchaProvider { - EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER' + EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER', + PHONE_PROVIDER = 'PHONE_PROVIDER' } export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000); diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts index 42a758840f6..b3c97d0716f 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts @@ -29,7 +29,7 @@ import { import { isV2, isEnterprise, RecaptchaConfig } from './recaptcha'; import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha'; -import { EnforcementState } from '../../api/index'; +import { EnforcementState, RecaptchaProvider } from '../../api/index'; use(chaiAsPromised); use(sinonChai); @@ -39,17 +39,60 @@ 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' } + { + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.ENFORCE + }, + { + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.AUDIT + } ] }; + const GET_RECAPTCHA_CONFIG_RESPONSE_OFF: GetRecaptchaConfigResponse = { + recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY, + recaptchaEnforcementState: [ + { + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.OFF + }, + { + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.OFF + } + ] + }; + + const GET_RECAPTCHA_CONFIG_RESPONSE_ENFORCE_AND_OFF: GetRecaptchaConfigResponse = + { + recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY, + recaptchaEnforcementState: [ + { + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.ENFORCE + }, + { + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.OFF + } + ] + }; + + const recaptchaConfig = new RecaptchaConfig(GET_RECAPTCHA_CONFIG_RESPONSE); + const recaptchaConfigOff = new RecaptchaConfig( + GET_RECAPTCHA_CONFIG_RESPONSE_OFF + ); + const recaptchaConfigEnforceAndOff = new RecaptchaConfig( + GET_RECAPTCHA_CONFIG_RESPONSE_ENFORCE_AND_OFF + ); + context('#verify', () => { beforeEach(async () => { auth = await testAuth(); @@ -74,30 +117,63 @@ describe('platform_browser/recaptcha/recaptcha', () => { }); 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' + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.ENFORCE + }); + expect(recaptchaConfig.recaptchaEnforcementState[1]).to.eql({ + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.AUDIT + }); + expect(recaptchaConfigEnforceAndOff.recaptchaEnforcementState[1]).to.eql({ + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.OFF }); }); it('#getProviderEnforcementState should return the correct enforcement state of the provider', () => { expect( - recaptchaConfig.getProviderEnforcementState('EMAIL_PASSWORD_PROVIDER') + recaptchaConfig.getProviderEnforcementState( + RecaptchaProvider.EMAIL_PASSWORD_PROVIDER + ) ).to.eq(EnforcementState.ENFORCE); + expect( + recaptchaConfig.getProviderEnforcementState( + RecaptchaProvider.PHONE_PROVIDER + ) + ).to.eq(EnforcementState.AUDIT); + expect( + recaptchaConfigEnforceAndOff.getProviderEnforcementState( + RecaptchaProvider.PHONE_PROVIDER + ) + ).to.eq(EnforcementState.OFF); 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( + RecaptchaProvider.EMAIL_PASSWORD_PROVIDER + ) + ).to.be.true; + expect( + recaptchaConfig.isProviderEnabled(RecaptchaProvider.PHONE_PROVIDER) + ).to.be.true; + expect( + recaptchaConfigEnforceAndOff.isProviderEnabled( + RecaptchaProvider.PHONE_PROVIDER + ) + ).to.be.false; expect(recaptchaConfig.isProviderEnabled('invalid-provider')).to.be.false; }); + + it('#isAnyProviderEnabled should return true if at least one provider is enabled', () => { + expect(recaptchaConfig.isAnyProviderEnabled()).to.be.true; + expect(recaptchaConfigEnforceAndOff.isAnyProviderEnabled()).to.be.true; + expect(recaptchaConfigOff.isAnyProviderEnabled()).to.be.false; + }); }); }); diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha.ts index bb1b79895c0..2cc47f8a0cd 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha.ts @@ -20,7 +20,11 @@ import { GetRecaptchaConfigResponse, RecaptchaEnforcementProviderState } from '../../api/authentication/recaptcha'; -import { EnforcementState, _parseEnforcementState } from '../../api/index'; +import { + EnforcementState, + RecaptchaProvider, + _parseEnforcementState +} from '../../api/index'; // reCAPTCHA v2 interface export interface Recaptcha { @@ -135,4 +139,17 @@ export class RecaptchaConfig { this.getProviderEnforcementState(providerStr) === EnforcementState.AUDIT ); } + + /** + * Returns true if reCAPTCHA Enterprise protection is enabled in at least one provider, otherwise + * returns false. + * + * @returns Whether or not reCAPTCHA Enterprise protection is enabled for at least one provider. + */ + isAnyProviderEnabled(): boolean { + return ( + this.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER) || + this.isProviderEnabled(RecaptchaProvider.PHONE_PROVIDER) + ); + } } diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.test.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.test.ts index ecbe6c0232d..3b351a8fac7 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.test.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.test.ts @@ -24,7 +24,9 @@ import { Endpoint, RecaptchaClientType, RecaptchaVersion, - RecaptchaActionName + RecaptchaActionName, + RecaptchaProvider, + EnforcementState } from '../../api'; import { mockEndpointWithParams } from '../../../test/helpers/api/helper'; import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; @@ -36,7 +38,8 @@ import { MockGreCAPTCHATopLevel } from './recaptcha_mock'; import { RecaptchaEnterpriseVerifier, FAKE_TOKEN, - handleRecaptchaFlow + handleRecaptchaFlow, + injectRecaptchaFields } from './recaptcha_enterprise_verifier'; import { RecaptchaConfig } from './recaptcha'; import { AuthErrorCode } from '../../core/errors'; @@ -53,8 +56,12 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { recaptchaKey: 'foo/bar/to/site-key', recaptchaEnforcementState: [ { - provider: 'EMAIL_PASSWORD_PROVIDER', - enforcementState: 'ENFORCE' + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.ENFORCE + }, + { + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.ENFORCE } ] }; @@ -65,17 +72,30 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { recaptchaKey: 'foo/bar/to/site-key', recaptchaEnforcementState: [ { - provider: 'EMAIL_PASSWORD_PROVIDER', - enforcementState: 'OFF' + provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER, + enforcementState: EnforcementState.OFF + }, + { + provider: RecaptchaProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.OFF } ] }; const recaptchaConfigOff = new RecaptchaConfig(recaptchaConfigResponseOff); + const getRecaptchaConfigRequest = { + clientType: RecaptchaClientType.WEB, + version: RecaptchaVersion.ENTERPRISE + }; + + let recaptcha: MockGreCAPTCHATopLevel; + beforeEach(async () => { auth = await testAuth(); mockFetch.setUp(); verifier = new RecaptchaEnterpriseVerifier(auth); + recaptcha = new MockGreCAPTCHATopLevel(); + window.grecaptcha = recaptcha; }); afterEach(() => { @@ -84,21 +104,10 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { }); context('#verify', () => { - const request = { - clientType: RecaptchaClientType.WEB, - version: RecaptchaVersion.ENTERPRISE - }; - - let recaptcha: MockGreCAPTCHATopLevel; - beforeEach(() => { - recaptcha = new MockGreCAPTCHATopLevel(); - window.grecaptcha = recaptcha; - }); - it('returns if response is available', async () => { mockEndpointWithParams( Endpoint.GET_RECAPTCHA_CONFIG, - request, + getRecaptchaConfigRequest, recaptchaConfigResponseEnforce ); sinon @@ -110,7 +119,7 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { it('reject if error is thrown when retrieve site key', async () => { mockEndpointWithParams( Endpoint.GET_RECAPTCHA_CONFIG, - request, + getRecaptchaConfigRequest, { error: { code: 400, @@ -131,7 +140,7 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { it('return fake recaptcha token if error is thrown when retrieve recaptcha token', async () => { mockEndpointWithParams( Endpoint.GET_RECAPTCHA_CONFIG, - request, + getRecaptchaConfigRequest, recaptchaConfigResponseEnforce ); sinon @@ -246,4 +255,146 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { expect(mockActionMethod).to.have.been.calledOnce; }); }); + + context('#injectRecaptchaFields', () => { + it('injects recaptcha enterprise fields into SignInWithPassword request', async () => { + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + getRecaptchaConfigRequest, + recaptchaConfigResponseEnforce + ); + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('recaptcha-response')); + + const request = { + returnSecureToken: true, + email: 'email', + password: 'password', + clientType: RecaptchaClientType.WEB + }; + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.SIGN_IN_WITH_PASSWORD, + false + ); + const expectedRequest = { + returnSecureToken: true, + email: 'email', + password: 'password', + clientType: RecaptchaClientType.WEB, + captchaResponse: 'recaptcha-response', + recaptchaVersion: RecaptchaVersion.ENTERPRISE + }; + + expect(requestWithRecaptcha).to.eql(expectedRequest); + }); + + it('injects recaptcha enterprise fields when captchaResp is true', async () => { + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + getRecaptchaConfigRequest, + recaptchaConfigResponseEnforce + ); + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('recaptcha-response')); + + const request = { + requestType: 'requestType', + email: 'email', + clientType: RecaptchaClientType.WEB + }; + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.GET_OOB_CODE, + true + ); + const expectedRequest = { + requestType: 'requestType', + email: 'email', + clientType: RecaptchaClientType.WEB, + captchaResp: 'recaptcha-response', + recaptchaVersion: RecaptchaVersion.ENTERPRISE + }; + + expect(requestWithRecaptcha).to.eql(expectedRequest); + }); + + it('injects recaptcha enterprise fields into StartPhoneMfaEnrollment request', async () => { + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + getRecaptchaConfigRequest, + recaptchaConfigResponseEnforce + ); + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('recaptcha-response')); + + const request = { + idToken: 'idToken', + phoneEnrollmentInfo: { + phoneNumber: '123456', + recaptchaToken: 'recaptchaToken' + } + }; + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.MFA_SMS_ENROLLMENT, + false + ); + const expectedRequest = { + idToken: 'idToken', + phoneEnrollmentInfo: { + phoneNumber: '123456', + recaptchaToken: 'recaptchaToken', + captchaResponse: 'recaptcha-response', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE + } + }; + + expect(requestWithRecaptcha).to.eql(expectedRequest); + }); + + it('injects recaptcha enterprise fields into StartPhoneMfaSignInRequest request', async () => { + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + getRecaptchaConfigRequest, + recaptchaConfigResponseEnforce + ); + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('recaptcha-response')); + + const request = { + mfaPendingCredential: 'mfaPendingCredential', + mfaEnrollmentId: 'mfaEnrollmentId', + phoneSignInInfo: { + recaptchaToken: 'recaptchaToken' + } + }; + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.MFA_SMS_SIGNIN, + false + ); + const expectedRequest = { + mfaPendingCredential: 'mfaPendingCredential', + mfaEnrollmentId: 'mfaEnrollmentId', + phoneSignInInfo: { + recaptchaToken: 'recaptchaToken', + captchaResponse: 'recaptcha-response', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE + } + }; + + expect(requestWithRecaptcha).to.eql(expectedRequest); + }); + }); }); 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 8d188032e36..b3b8f97bae8 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts @@ -30,6 +30,8 @@ import { AuthInternal } from '../../model/auth'; import { _castAuth } from '../../core/auth/auth_impl'; import * as jsHelpers from '../load_js'; import { AuthErrorCode } from '../../core/errors'; +import { StartPhoneMfaEnrollmentRequest } from '../../api/account_management/mfa'; +import { StartPhoneMfaSignInRequest } from '../../api/authentication/mfa'; const RECAPTCHA_ENTERPRISE_URL = 'https://www.google.com/recaptcha/enterprise.js?render='; @@ -155,17 +157,62 @@ export async function injectRecaptchaFields( auth: AuthInternal, request: T, action: RecaptchaActionName, - captchaResp = false + isCaptchaResp = false, + isFakeToken = false ): Promise { const verifier = new RecaptchaEnterpriseVerifier(auth); let captchaResponse; - try { - captchaResponse = await verifier.verify(action); - } catch (error) { - captchaResponse = await verifier.verify(action, true); + + if (isFakeToken) { + captchaResponse = FAKE_TOKEN; + } else { + try { + captchaResponse = await verifier.verify(action); + } catch (error) { + captchaResponse = await verifier.verify(action, true); + } } + const newRequest = { ...request }; - if (!captchaResp) { + if ( + action === RecaptchaActionName.MFA_SMS_ENROLLMENT || + action === RecaptchaActionName.MFA_SMS_SIGNIN + ) { + if ('phoneEnrollmentInfo' in newRequest) { + const phoneNumber = ( + newRequest as unknown as StartPhoneMfaEnrollmentRequest + ).phoneEnrollmentInfo.phoneNumber; + const recaptchaToken = ( + newRequest as unknown as StartPhoneMfaEnrollmentRequest + ).phoneEnrollmentInfo.recaptchaToken; + + Object.assign(newRequest, { + 'phoneEnrollmentInfo': { + phoneNumber, + recaptchaToken, + captchaResponse, + 'clientType': RecaptchaClientType.WEB, + 'recaptchaVersion': RecaptchaVersion.ENTERPRISE + } + }); + } else if ('phoneSignInInfo' in newRequest) { + const recaptchaToken = ( + newRequest as unknown as StartPhoneMfaSignInRequest + ).phoneSignInInfo.recaptchaToken; + + Object.assign(newRequest, { + 'phoneSignInInfo': { + recaptchaToken, + captchaResponse, + 'clientType': RecaptchaClientType.WEB, + 'recaptchaVersion': RecaptchaVersion.ENTERPRISE + } + }); + } + return newRequest; + } + + if (!isCaptchaResp) { Object.assign(newRequest, { captchaResponse }); } else { Object.assign(newRequest, { 'captchaResp': captchaResponse }); @@ -235,7 +282,7 @@ export async function _initializeRecaptchaConfig(auth: Auth): Promise { authInternal._tenantRecaptchaConfigs[authInternal.tenantId] = config; } - if (config.isProviderEnabled(RecaptchaProvider.EMAIL_PASSWORD_PROVIDER)) { + if (config.isAnyProviderEnabled()) { const verifier = new RecaptchaEnterpriseVerifier(authInternal); void verifier.verify(); }