diff --git a/common/api-review/auth.api.md b/common/api-review/auth.api.md index 8e915daf731..b011b803774 100644 --- a/common/api-review/auth.api.md +++ b/common/api-review/auth.api.md @@ -445,7 +445,7 @@ export function isSignInWithEmailLink(auth: Auth, emailLink: string): boolean; export function linkWithCredential(user: User, credential: AuthCredential): Promise; // @public -export function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; // @public export function linkWithPopup(user: User, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise; @@ -625,7 +625,7 @@ export class PhoneAuthProvider { static readonly PHONE_SIGN_IN_METHOD: 'phone'; static readonly PROVIDER_ID: 'phone'; readonly providerId: "phone"; - verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier: ApplicationVerifier): Promise; + verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier?: ApplicationVerifier): Promise; } // @public @@ -692,7 +692,7 @@ export interface ReactNativeAsyncStorage { export function reauthenticateWithCredential(user: User, credential: AuthCredential): Promise; // @public -export function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; // @public export function reauthenticateWithPopup(user: User, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise; @@ -778,7 +778,7 @@ export function signInWithEmailAndPassword(auth: Auth, email: string, password: export function signInWithEmailLink(auth: Auth, email: string, emailLink?: string): Promise; // @public -export function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; // @public export function signInWithPopup(auth: Auth, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise; diff --git a/docs-devsite/auth.md b/docs-devsite/auth.md index 43d23dc8931..308badfc946 100644 --- a/docs-devsite/auth.md +++ b/docs-devsite/auth.md @@ -930,7 +930,7 @@ This method does not work in a Node.js environment or with [Auth](./auth.auth.md Signature: ```typescript -export declare function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export declare function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; ``` #### Parameters @@ -1304,7 +1304,7 @@ This method does not work in a Node.js environment. Signature: ```typescript -export declare function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export declare function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; ``` #### Parameters @@ -1457,7 +1457,7 @@ This method does not work in a Node.js environment or on any [User](./auth.user. Signature: ```typescript -export declare function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise; +export declare function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise; ``` #### Parameters diff --git a/docs-devsite/auth.phoneauthprovider.md b/docs-devsite/auth.phoneauthprovider.md index 940e8e5442f..e64f869303b 100644 --- a/docs-devsite/auth.phoneauthprovider.md +++ b/docs-devsite/auth.phoneauthprovider.md @@ -203,7 +203,7 @@ Starts a phone number authentication flow by sending a verification code to the Signature: ```typescript -verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier: ApplicationVerifier): Promise; +verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier?: ApplicationVerifier): Promise; ``` #### Parameters diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index d1cce3161f4..610a59578a8 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -89,7 +89,7 @@ export const enum RecaptchaActionName { SIGN_UP_PASSWORD = 'signUpPassword', SEND_VERIFICATION_CODE = 'sendVerificationCode', MFA_SMS_ENROLLMENT = 'mfaSmsEnrollment', - MFA_SMS_SIGNIN = 'mfaSmsSignin' + MFA_SMS_SIGNIN = 'mfaSmsSignIn' } export const enum EnforcementState { diff --git a/packages/auth/src/platform_browser/providers/phone.test.ts b/packages/auth/src/platform_browser/providers/phone.test.ts index 8a75fa14871..9f555920ef3 100644 --- a/packages/auth/src/platform_browser/providers/phone.test.ts +++ b/packages/auth/src/platform_browser/providers/phone.test.ts @@ -15,9 +15,12 @@ * limitations under the License. */ -import { expect } from 'chai'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; +import { FirebaseError } from '@firebase/util'; + import { mockEndpoint, mockEndpointWithParams @@ -37,6 +40,8 @@ import { FAKE_TOKEN } from '../recaptcha/recaptcha_enterprise_verifier'; import { MockGreCAPTCHATopLevel } from '../recaptcha/recaptcha_mock'; import { ApplicationVerifierInternal } from '../../model/application_verifier'; +use(chaiAsPromised); + describe('platform_browser/providers/phone', () => { let auth: TestAuth; let v2Verifier: ApplicationVerifierInternal; @@ -104,6 +109,83 @@ describe('platform_browser/providers/phone', () => { }); }); + it('throws an error if verify without appVerifier when recaptcha enterprise is disabled', async () => { + const recaptchaConfigResponseOff = { + recaptchaKey: 'foo/bar/to/site-key', + recaptchaEnforcementState: [ + { + provider: RecaptchaAuthProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.OFF + } + ] + }; + const recaptcha = new MockGreCAPTCHATopLevel(); + if (typeof window === 'undefined') { + return; + } + window.grecaptcha = recaptcha; + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('enterprise-token')); + + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + { + clientType: RecaptchaClientType.WEB, + version: RecaptchaVersion.ENTERPRISE + }, + recaptchaConfigResponseOff + ); + + const provider = new PhoneAuthProvider(auth); + await expect( + provider.verifyPhoneNumber('+15105550000') + ).to.be.rejectedWith(FirebaseError, 'auth/argument-error'); + }); + + it('calls the server without appVerifier when recaptcha enterprise is enabled', async () => { + const recaptchaConfigResponseEnforce = { + recaptchaKey: 'foo/bar/to/site-key', + recaptchaEnforcementState: [ + { + provider: RecaptchaAuthProvider.PHONE_PROVIDER, + enforcementState: EnforcementState.ENFORCE + } + ] + }; + const recaptcha = new MockGreCAPTCHATopLevel(); + if (typeof window === 'undefined') { + return; + } + window.grecaptcha = recaptcha; + sinon + .stub(recaptcha.enterprise, 'execute') + .returns(Promise.resolve('enterprise-token')); + + mockEndpointWithParams( + Endpoint.GET_RECAPTCHA_CONFIG, + { + clientType: RecaptchaClientType.WEB, + version: RecaptchaVersion.ENTERPRISE + }, + recaptchaConfigResponseEnforce + ); + + const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, { + sessionInfo: 'verification-id' + }); + + const provider = new PhoneAuthProvider(auth); + const result = await provider.verifyPhoneNumber('+15105550000'); + expect(result).to.eq('verification-id'); + expect(route.calls[0].request).to.eql({ + phoneNumber: '+15105550000', + captchaResponse: 'enterprise-token', + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE + }); + }); + it('calls the server when recaptcha enterprise is enabled', async () => { const recaptchaConfigResponseEnforce = { recaptchaKey: 'foo/bar/to/site-key', diff --git a/packages/auth/src/platform_browser/providers/phone.ts b/packages/auth/src/platform_browser/providers/phone.ts index 82b05385796..a9b2f253f8a 100644 --- a/packages/auth/src/platform_browser/providers/phone.ts +++ b/packages/auth/src/platform_browser/providers/phone.ts @@ -104,7 +104,7 @@ export class PhoneAuthProvider { */ verifyPhoneNumber( phoneOptions: PhoneInfoOptions | string, - applicationVerifier: ApplicationVerifier + applicationVerifier?: ApplicationVerifier ): Promise { return _verifyPhoneNumber( this.auth, diff --git a/packages/auth/src/platform_browser/strategies/phone.test.ts b/packages/auth/src/platform_browser/strategies/phone.test.ts index 96d887613d0..05728ee2dae 100644 --- a/packages/auth/src/platform_browser/strategies/phone.test.ts +++ b/packages/auth/src/platform_browser/strategies/phone.test.ts @@ -181,6 +181,32 @@ describe('platform_browser/strategies/phone', () => { }); }); + it('calls verify phone number without a v2 RecaptchaVerifier when recaptcha enterprise is enabled', async () => { + if (typeof window === 'undefined') { + return; + } + mockRecaptchaEnterpriseEnablement(EnforcementState.ENFORCE); + await signInWithPhoneNumber(auth, '+15105550000'); + + expect(sendCodeEndpoint.calls[0].request).to.eql({ + phoneNumber: '+15105550000', + captchaResponse: RECAPTCHA_ENTERPRISE_TOKEN, + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE + }); + }); + + it('throws an error if verify phone number without a v2 RecaptchaVerifier when recaptcha enterprise is disabled', async () => { + if (typeof window === 'undefined') { + return; + } + mockRecaptchaEnterpriseEnablement(EnforcementState.OFF); + + await expect( + signInWithPhoneNumber(auth, '+15105550000') + ).to.be.rejectedWith(FirebaseError, 'auth/argument-error'); + }); + context('ConfirmationResult', () => { it('result contains verification id baked in', async () => { if (typeof window === 'undefined') { @@ -504,6 +530,33 @@ describe('platform_browser/strategies/phone', () => { }); }); + it('works without v2 RecaptchaVerifier when recaptcha enterprise is enabled', async () => { + if (typeof window === 'undefined') { + return; + } + mockRecaptchaEnterpriseEnablement(EnforcementState.ENFORCE); + const sessionInfo = await _verifyPhoneNumber(auth, 'number'); + expect(sessionInfo).to.eq('session-info'); + expect(sendCodeEndpoint.calls[0].request).to.eql({ + phoneNumber: 'number', + captchaResponse: RECAPTCHA_ENTERPRISE_TOKEN, + clientType: RecaptchaClientType.WEB, + recaptchaVersion: RecaptchaVersion.ENTERPRISE + }); + }); + + it('throws error if calls verify phone number without v2 RecaptchaVerifier when recaptcha enterprise is disabled', async () => { + if (typeof window === 'undefined') { + return; + } + mockRecaptchaEnterpriseEnablement(EnforcementState.OFF); + + await expect(_verifyPhoneNumber(auth, 'number')).to.be.rejectedWith( + FirebaseError, + 'auth/argument-error' + ); + }); + it('calls fallback to recaptcha v2 flow when receiving MISSING_RECAPTCHA_TOKEN error in recaptcha enterprise audit mode', async () => { if (typeof window === 'undefined') { return; diff --git a/packages/auth/src/platform_browser/strategies/phone.ts b/packages/auth/src/platform_browser/strategies/phone.ts index a074eca9e7e..e661499fdaa 100644 --- a/packages/auth/src/platform_browser/strategies/phone.ts +++ b/packages/auth/src/platform_browser/strategies/phone.ts @@ -129,7 +129,7 @@ class ConfirmationResultImpl implements ConfirmationResult { export async function signInWithPhoneNumber( auth: Auth, phoneNumber: string, - appVerifier: ApplicationVerifier + appVerifier?: ApplicationVerifier ): Promise { if (_isFirebaseServerApp(auth.app)) { return Promise.reject( @@ -162,7 +162,7 @@ export async function signInWithPhoneNumber( export async function linkWithPhoneNumber( user: User, phoneNumber: string, - appVerifier: ApplicationVerifier + appVerifier?: ApplicationVerifier ): Promise { const userInternal = getModularInstance(user) as UserInternal; await _assertLinkedStatus(false, userInternal, ProviderId.PHONE); @@ -194,7 +194,7 @@ export async function linkWithPhoneNumber( export async function reauthenticateWithPhoneNumber( user: User, phoneNumber: string, - appVerifier: ApplicationVerifier + appVerifier?: ApplicationVerifier ): Promise { const userInternal = getModularInstance(user) as UserInternal; if (_isFirebaseServerApp(userInternal.auth.app)) { @@ -224,7 +224,7 @@ type PhoneApiCaller = ( export async function _verifyPhoneNumber( auth: AuthInternal, options: PhoneInfoOptions | string, - verifier: ApplicationVerifierInternal + verifier?: ApplicationVerifierInternal ): Promise { if (!auth._getRecaptchaConfig()) { const enterpriseVerifier = new RecaptchaEnterpriseVerifier(auth); @@ -274,7 +274,7 @@ export async function _verifyPhoneNumber( request.phoneEnrollmentInfo.captchaResponse === FAKE_TOKEN ) { _assert( - verifier.type === RECAPTCHA_VERIFIER_TYPE, + verifier?.type === RECAPTCHA_VERIFIER_TYPE, authInstance, AuthErrorCode.ARGUMENT_ERROR ); @@ -329,14 +329,14 @@ export async function _verifyPhoneNumber( authInstance: AuthInternal, request: StartPhoneMfaSignInRequest ) => { - // If reCAPTCHA Enterprise token is empty or "NO_RECAPTCHA", fetch v2 token and inject into request. + // If reCAPTCHA Enterprise token is empty or "NO_RECAPTCHA", fetch reCAPTCHA v2 token and inject into request. if ( !request.phoneSignInInfo.captchaResponse || request.phoneSignInInfo.captchaResponse.length === 0 || request.phoneSignInInfo.captchaResponse === FAKE_TOKEN ) { _assert( - verifier.type === RECAPTCHA_VERIFIER_TYPE, + verifier?.type === RECAPTCHA_VERIFIER_TYPE, authInstance, AuthErrorCode.ARGUMENT_ERROR ); @@ -380,14 +380,14 @@ export async function _verifyPhoneNumber( authInstance: AuthInternal, request: SendPhoneVerificationCodeRequest ) => { - // If reCAPTCHA Enterprise token is empty or "NO_RECAPTCHA", fetch v2 token and inject into request. + // If reCAPTCHA Enterprise token is empty or "NO_RECAPTCHA", fetch reCAPTCHA v2 token and inject into request. if ( !request.captchaResponse || request.captchaResponse.length === 0 || request.captchaResponse === FAKE_TOKEN ) { _assert( - verifier.type === RECAPTCHA_VERIFIER_TYPE, + verifier?.type === RECAPTCHA_VERIFIER_TYPE, authInstance, AuthErrorCode.ARGUMENT_ERROR ); @@ -421,7 +421,7 @@ export async function _verifyPhoneNumber( return response.sessionInfo; } } finally { - verifier._reset(); + verifier?._reset(); } }