diff --git a/.changeset/lucky-dragons-juggle.md b/.changeset/lucky-dragons-juggle.md deleted file mode 100644 index f21b661d465..00000000000 --- a/.changeset/lucky-dragons-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/auth': patch ---- - -Create handleRecaptchaFlow helper method diff --git a/packages/auth/src/core/credentials/email.ts b/packages/auth/src/core/credentials/email.ts index edcde2ea053..6421f33b5a1 100644 --- a/packages/auth/src/core/credentials/email.ts +++ b/packages/auth/src/core/credentials/email.ts @@ -31,7 +31,7 @@ import { IdTokenResponse } from '../../model/id_token'; import { AuthErrorCode } from '../errors'; import { _fail } from '../util/assert'; import { AuthCredential } from './auth_credential'; -import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; +import { injectRecaptchaFields } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; import { RecaptchaActionName, RecaptchaClientType } from '../../api'; /** * Interface that represents the credentials returned by {@link EmailAuthProvider} for @@ -123,12 +123,32 @@ export class EmailAuthCredential extends AuthCredential { password: this._password, clientType: RecaptchaClientType.WEB }; - return handleRecaptchaFlow( - auth, - request, - RecaptchaActionName.SIGN_IN_WITH_PASSWORD, - signInWithPassword - ); + if (auth._getRecaptchaConfig()?.emailPasswordEnabled) { + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.SIGN_IN_WITH_PASSWORD + ); + return signInWithPassword(auth, requestWithRecaptcha); + } else { + return signInWithPassword(auth, request).catch(async error => { + if ( + error.code === `auth/${AuthErrorCode.MISSING_RECAPTCHA_TOKEN}` + ) { + console.log( + 'Sign-in with email address and password is protected by reCAPTCHA for this project. Automatically triggering the reCAPTCHA flow and restarting the sign-in flow.' + ); + const requestWithRecaptcha = await injectRecaptchaFields( + auth, + request, + RecaptchaActionName.SIGN_IN_WITH_PASSWORD + ); + return signInWithPassword(auth, requestWithRecaptcha); + } else { + return Promise.reject(error); + } + }); + } case SignInMethod.EMAIL_LINK: return signInWithEmailLink(auth, { email: this._email, diff --git a/packages/auth/src/core/strategies/email_and_password.ts b/packages/auth/src/core/strategies/email_and_password.ts index 0ff21810a49..33a2ef8af8d 100644 --- a/packages/auth/src/core/strategies/email_and_password.ts +++ b/packages/auth/src/core/strategies/email_and_password.ts @@ -36,7 +36,7 @@ import { _castAuth } from '../auth/auth_impl'; import { AuthErrorCode } from '../errors'; import { getModularInstance } from '@firebase/util'; import { OperationType } from '../../model/enums'; -import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; +import { injectRecaptchaFields } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; import { IdTokenResponse } from '../../model/id_token'; import { RecaptchaActionName, RecaptchaClientType } from '../../api'; @@ -103,15 +103,61 @@ export async function sendPasswordResetEmail( email, clientType: RecaptchaClientType.WEB }; - if (actionCodeSettings) { - _setActionCodeSettingsOnRequest(authInternal, request, actionCodeSettings); + if (authInternal._getRecaptchaConfig()?.emailPasswordEnabled) { + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.GET_OOB_CODE, + true + ); + if (actionCodeSettings) { + _setActionCodeSettingsOnRequest( + authInternal, + requestWithRecaptcha, + actionCodeSettings + ); + } + await authentication.sendPasswordResetEmail( + authInternal, + requestWithRecaptcha + ); + } else { + if (actionCodeSettings) { + _setActionCodeSettingsOnRequest( + authInternal, + request, + actionCodeSettings + ); + } + await authentication + .sendPasswordResetEmail(authInternal, request) + .catch(async error => { + if (error.code === `auth/${AuthErrorCode.MISSING_RECAPTCHA_TOKEN}`) { + console.log( + 'Password resets are protected by reCAPTCHA for this project. Automatically triggering the reCAPTCHA flow and restarting the password reset flow.' + ); + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.GET_OOB_CODE, + true + ); + if (actionCodeSettings) { + _setActionCodeSettingsOnRequest( + authInternal, + requestWithRecaptcha, + actionCodeSettings + ); + } + await authentication.sendPasswordResetEmail( + authInternal, + requestWithRecaptcha + ); + } else { + return Promise.reject(error); + } + }); } - await handleRecaptchaFlow( - authInternal, - request, - RecaptchaActionName.GET_OOB_CODE, - authentication.sendPasswordResetEmail - ); } /** @@ -272,12 +318,32 @@ export async function createUserWithEmailAndPassword( password, clientType: RecaptchaClientType.WEB }; - const signUpResponse: Promise = handleRecaptchaFlow( - authInternal, - request, - RecaptchaActionName.SIGN_UP_PASSWORD, - signUp - ); + let signUpResponse: Promise; + if (authInternal._getRecaptchaConfig()?.emailPasswordEnabled) { + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.SIGN_UP_PASSWORD + ); + signUpResponse = signUp(authInternal, requestWithRecaptcha); + } else { + signUpResponse = signUp(authInternal, request).catch(async error => { + if (error.code === `auth/${AuthErrorCode.MISSING_RECAPTCHA_TOKEN}`) { + console.log( + 'Sign-up is protected by reCAPTCHA for this project. Automatically triggering the reCAPTCHA flow and restarting the sign-up flow.' + ); + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.SIGN_UP_PASSWORD + ); + return signUp(authInternal, requestWithRecaptcha); + } + + throw error; + }); + } + const response = await signUpResponse.catch(error => { if ( error.code === `auth/${AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS}` diff --git a/packages/auth/src/core/strategies/email_link.ts b/packages/auth/src/core/strategies/email_link.ts index 55f95226656..f67a1e3ea03 100644 --- a/packages/auth/src/core/strategies/email_link.ts +++ b/packages/auth/src/core/strategies/email_link.ts @@ -32,7 +32,7 @@ import { AuthErrorCode } from '../errors'; import { _assert } from '../util/assert'; import { getModularInstance } from '@firebase/util'; import { _castAuth } from '../auth/auth_impl'; -import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; +import { injectRecaptchaFields } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier'; import { RecaptchaActionName, RecaptchaClientType } from '../../api'; /** @@ -101,13 +101,37 @@ export async function sendSignInLinkToEmail( ); } } - setActionCodeSettings(request, actionCodeSettings); - await handleRecaptchaFlow( - authInternal, - request, - RecaptchaActionName.GET_OOB_CODE, - api.sendSignInLinkToEmail - ); + if (authInternal._getRecaptchaConfig()?.emailPasswordEnabled) { + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.GET_OOB_CODE, + true + ); + setActionCodeSettings(requestWithRecaptcha, actionCodeSettings); + await api.sendSignInLinkToEmail(authInternal, requestWithRecaptcha); + } else { + setActionCodeSettings(request, actionCodeSettings); + await api + .sendSignInLinkToEmail(authInternal, request) + .catch(async error => { + if (error.code === `auth/${AuthErrorCode.MISSING_RECAPTCHA_TOKEN}`) { + console.log( + 'Email link sign-in is protected by reCAPTCHA for this project. Automatically triggering the reCAPTCHA flow and restarting the sign-in flow.' + ); + const requestWithRecaptcha = await injectRecaptchaFields( + authInternal, + request, + RecaptchaActionName.GET_OOB_CODE, + true + ); + setActionCodeSettings(requestWithRecaptcha, actionCodeSettings); + await api.sendSignInLinkToEmail(authInternal, requestWithRecaptcha); + } else { + return Promise.reject(error); + } + }); + } } /** 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 7853e7ea342..b1cbd959c4c 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 @@ -20,23 +20,16 @@ import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { - Endpoint, - RecaptchaClientType, - RecaptchaVersion, - RecaptchaActionName -} from '../../api'; +import { Endpoint, RecaptchaClientType, RecaptchaVersion } from '../../api'; import { mockEndpointWithParams } from '../../../test/helpers/api/helper'; import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; import * as mockFetch from '../../../test/helpers/mock_fetch'; import { ServerError } from '../../api/errors'; -import { AuthInternal } from '../../model/auth'; import { MockGreCAPTCHATopLevel } from './recaptcha_mock'; import { RecaptchaEnterpriseVerifier, - FAKE_TOKEN, - handleRecaptchaFlow + FAKE_TOKEN } from './recaptcha_enterprise_verifier'; use(chaiAsPromised); @@ -124,86 +117,4 @@ describe('platform_browser/recaptcha/recaptcha_enterprise_verifier', () => { expect(await verifier.verify()).to.eq(FAKE_TOKEN); }); }); - - context('handleRecaptchaFlow', () => { - let mockAuthInstance: AuthInternal; - let mockRequest: any; - let mockActionMethod: sinon.SinonStub; - - beforeEach(async () => { - mockAuthInstance = await testAuth(); - mockRequest = {}; - mockActionMethod = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('should handle recaptcha when emailPasswordEnabled is true', async () => { - if (typeof window === 'undefined') { - return; - } - sinon.stub(mockAuthInstance, '_getRecaptchaConfig').returns({ - emailPasswordEnabled: true, - siteKey: 'mock_site_key' - }); - mockActionMethod.resolves('success'); - - const result = await handleRecaptchaFlow( - mockAuthInstance, - mockRequest, - RecaptchaActionName.GET_OOB_CODE, - mockActionMethod - ); - - expect(result).to.equal('success'); - expect(mockActionMethod).to.have.been.calledOnce; - }); - - it('should handle action without recaptcha when emailPasswordEnabled is false and no error', async () => { - if (typeof window === 'undefined') { - return; - } - sinon.stub(mockAuthInstance, '_getRecaptchaConfig').returns({ - emailPasswordEnabled: false, - siteKey: 'mock_site_key' - }); - mockActionMethod.resolves('success'); - - const result = await handleRecaptchaFlow( - mockAuthInstance, - mockRequest, - RecaptchaActionName.GET_OOB_CODE, - mockActionMethod - ); - - expect(result).to.equal('success'); - expect(mockActionMethod).to.have.been.calledOnce; - }); - - it('should handle MISSING_RECAPTCHA_TOKEN error when emailPasswordEnabled is false', async () => { - if (typeof window === 'undefined') { - return; - } - sinon.stub(mockAuthInstance, '_getRecaptchaConfig').returns({ - emailPasswordEnabled: false, - siteKey: 'mock_site_key' - }); - mockActionMethod.onFirstCall().rejects({ - code: 'auth/MISSING_RECAPTCHA_TOKEN' - }); - mockActionMethod.onSecondCall().resolves('success-after-recaptcha'); - - const result = await handleRecaptchaFlow( - mockAuthInstance, - mockRequest, - RecaptchaActionName.GET_OOB_CODE, - mockActionMethod - ); - - expect(result).to.equal('success-after-recaptcha'); - expect(mockActionMethod).to.have.been.calledTwice; - }); - }); }); 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 63ec91fceae..c2c0303088a 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts @@ -28,7 +28,6 @@ import { Auth } from '../../model/public_types'; import { AuthInternal } from '../../model/auth'; import { _castAuth } from '../../core/auth/auth_impl'; import * as jsHelpers from '../load_js'; -import { AuthErrorCode } from '../../core/errors'; const RECAPTCHA_ENTERPRISE_URL = 'https://www.google.com/recaptcha/enterprise.js?render='; @@ -176,45 +175,6 @@ export async function injectRecaptchaFields( return newRequest; } -type ActionMethod = ( - auth: Auth, - request: TRequest -) => Promise; - -export async function handleRecaptchaFlow( - authInstance: AuthInternal, - request: TRequest, - actionName: RecaptchaActionName, - actionMethod: ActionMethod -): Promise { - if (authInstance._getRecaptchaConfig()?.emailPasswordEnabled) { - const requestWithRecaptcha = await injectRecaptchaFields( - authInstance, - request, - actionName, - actionName === RecaptchaActionName.GET_OOB_CODE - ); - return actionMethod(authInstance, requestWithRecaptcha); - } else { - return actionMethod(authInstance, request).catch(async error => { - if (error.code === `auth/${AuthErrorCode.MISSING_RECAPTCHA_TOKEN}`) { - console.log( - `${actionName} is protected by reCAPTCHA Enterprise for this project. Automatically triggering the reCAPTCHA flow and restarting the flow.` - ); - const requestWithRecaptcha = await injectRecaptchaFields( - authInstance, - request, - actionName, - actionName === RecaptchaActionName.GET_OOB_CODE - ); - return actionMethod(authInstance, requestWithRecaptcha); - } else { - return Promise.reject(error); - } - }); - } -} - export async function _initializeRecaptchaConfig(auth: Auth): Promise { const authInternal = _castAuth(auth);