Skip to content

Commit

Permalink
Stop calling GetRecaptchaConfig in phone auth when cached config is e…
Browse files Browse the repository at this point in the history
…mpty
  • Loading branch information
NhienLam committed Sep 17, 2024
1 parent 42e4814 commit 7bbb1b2
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 65 deletions.
10 changes: 8 additions & 2 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,14 @@ Loads the reCAPTCHA configuration into the `Auth` instance.

This will load the reCAPTCHA config, which indicates whether the reCAPTCHA verification flow should be triggered for each auth provider, into the current Auth session.

If initializeRecaptchaConfig() is not invoked, the auth flow will always start without reCAPTCHA verification. If the provider is configured to require reCAPTCHA verification, the SDK will transparently load the reCAPTCHA config and restart the auth flows.
For email auth, if initializeRecaptchaConfig() is not invoked, the auth flow will always start without reCAPTCHA verification. If the provider is configured to require reCAPTCHA verification, the SDK will transparently load the reCAPTCHA config and restart the auth flows.

Thus, by calling this optional method, you will reduce the latency of future auth flows. Loading the reCAPTCHA config early will also enhance the signal collected by reCAPTCHA.

For phone auth, if initializeRecaptchaConfig() is not invoked, the auth flow will always use reCAPTCHA v2 verification. If the provider is configured to require reCAPTCHA Enterprise verification, the phone verification will fail.

Thus, calling this method early is required for reCAPTCHA Enterprise verification in phone auth flows.

This method does not work in a Node.js environment.

<b>Signature:</b>
Expand Down Expand Up @@ -923,7 +927,9 @@ Asynchronously signs in using a phone number.

This method sends a code via SMS to the given phone number, and returns a [ConfirmationResult](./auth.confirmationresult.md#confirmationresult_interface)<!-- -->. After the user provides the code sent to their phone, call [ConfirmationResult.confirm()](./auth.confirmationresult.md#confirmationresultconfirm) with the code to sign the user in.

For abuse prevention, this method also requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes a reCAPTCHA-based implementation, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. This function can work on other platforms that do not support the [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class) (like React Native), but you need to use a third-party [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) implementation.
For abuse prevention with reCAPTCHA v2, this method also requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes a reCAPTCHA-v2-based implementation, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. This function can work on other platforms that do not support the [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class) (like React Native), but you need to use a third-party [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) implementation.

For abuse prevention with reCAPTCHA Enterprise, [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) is not required, depending on the enforcement state. However, [initializeRecaptchaConfig()](./auth.md#initializerecaptchaconfig_2a61ea7) must be called once before initiating reCAPTCHA Enterprise verification.

This method does not work in a Node.js environment or with [Auth](./auth.auth.md#auth_interface) instances created with a [FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface)<!-- -->.

Expand Down
2 changes: 1 addition & 1 deletion docs-devsite/auth.phoneauthprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier?:
| Parameter | Type | Description |
| --- | --- | --- |
| phoneOptions | [PhoneInfoOptions](./auth.md#phoneinfooptions) \| string | |
| applicationVerifier | [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) | For abuse prevention, this method also requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes a reCAPTCHA-based implementation, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. |
| applicationVerifier | [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) | For abuse prevention with reCAPTCHA v2, this method also requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes a reCAPTCHA-based implementation, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. For abuse prevention with reCAPTCHA Enterprise, [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) is not required, depending on the enforcement state. However, [initializeRecaptchaConfig()](./auth.md#initializerecaptchaconfig_2a61ea7) must be called once before initiating reCAPTCHA Enterprise verification. |

<b>Returns:</b>

Expand Down
9 changes: 8 additions & 1 deletion packages/auth/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,21 @@ export function setPersistence(
* verification flow should be triggered for each auth provider, into the
* current Auth session.
*
* If initializeRecaptchaConfig() is not invoked, the auth flow will always start
* For email auth, if initializeRecaptchaConfig() is not invoked, the auth flow will always start
* without reCAPTCHA verification. If the provider is configured to require reCAPTCHA
* verification, the SDK will transparently load the reCAPTCHA config and restart the
* auth flows.
*
* Thus, by calling this optional method, you will reduce the latency of future auth flows.
* Loading the reCAPTCHA config early will also enhance the signal collected by reCAPTCHA.
*
* For phone auth, if initializeRecaptchaConfig() is not invoked, the auth flow will always use
* reCAPTCHA v2 verification. If the provider is configured to require reCAPTCHA Enterprise
* verification, the phone verification will fail.
*
* Thus, calling this method early is required for reCAPTCHA Enterprise verification in phone auth
* flows.
*
* This method does not work in a Node.js environment.
*
* @example
Expand Down
255 changes: 216 additions & 39 deletions packages/auth/src/platform_browser/providers/phone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ import {
RecaptchaAuthProvider,
EnforcementState
} from '../../api';
import { ServerError } from '../../api/errors';
import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier';
import { PhoneAuthProvider } from './phone';
import { FAKE_TOKEN } from '../recaptcha/recaptcha_enterprise_verifier';
import {
FAKE_TOKEN,
_initializeRecaptchaConfig
} from '../recaptcha/recaptcha_enterprise_verifier';
import { MockGreCAPTCHATopLevel } from '../recaptcha/recaptcha_mock';
import { ApplicationVerifierInternal } from '../../model/application_verifier';

Expand All @@ -46,6 +50,36 @@ describe('platform_browser/providers/phone', () => {
let auth: TestAuth;
let v2Verifier: ApplicationVerifierInternal;

const recaptchaConfigResponseEnforce = {
recaptchaKey: 'foo/bar/to/site-key',
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.ENFORCE
}
]
};

const recaptchaConfigResponseAudit = {
recaptchaKey: 'foo/bar/to/site-key',
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.AUDIT
}
]
};

const recaptchaConfigResponseOff = {
// no recaptcha key if no rCE provider is enabled
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
};

beforeEach(async () => {
fetch.setUp();
auth = await testAuth();
Expand All @@ -63,15 +97,6 @@ describe('platform_browser/providers/phone', () => {

context('#verifyPhoneNumber', () => {
it('calls verify on the appVerifier and then calls the server when recaptcha enterprise is disabled', async () => {
const recaptchaConfigResponseOff = {
// no recaptcha key if no rCE provider is enabled
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
};
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
Expand Down Expand Up @@ -110,15 +135,6 @@ describe('platform_browser/providers/phone', () => {
});

it('throws an error if verify without appVerifier when recaptcha enterprise is disabled', async () => {
const recaptchaConfigResponseOff = {
// no recaptcha key if no rCE provider is enabled
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
};
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
Expand All @@ -143,16 +159,7 @@ describe('platform_browser/providers/phone', () => {
).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
}
]
};
it('calls the server without appVerifier when recaptcha enterprise is enforced', async () => {
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
Expand All @@ -170,6 +177,7 @@ describe('platform_browser/providers/phone', () => {
},
recaptchaConfigResponseEnforce
);
await _initializeRecaptchaConfig(auth);

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
Expand All @@ -186,16 +194,7 @@ describe('platform_browser/providers/phone', () => {
});
});

it('calls the server when recaptcha enterprise is enabled', async () => {
const recaptchaConfigResponseEnforce = {
recaptchaKey: 'foo/bar/to/site-key',
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.ENFORCE
}
]
};
it('calls the server when recaptcha enterprise is enforced', async () => {
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
Expand All @@ -213,6 +212,7 @@ describe('platform_browser/providers/phone', () => {
},
recaptchaConfigResponseEnforce
);
await _initializeRecaptchaConfig(auth);

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
Expand All @@ -231,6 +231,183 @@ describe('platform_browser/providers/phone', () => {
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});

/* Test cases when initializeRecaptchaConfig is not called before phone verification */
it('throws invalid-recaptcha-token when recaptcha enterprise is enforced, but initializeRecaptchaConfig was not called', async () => {
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
);
// Not call initializeRecaptchaConfig

const failureMock = mockEndpoint(
Endpoint.SEND_VERIFICATION_CODE,
{
error: {
code: 400,
message: ServerError.INVALID_RECAPTCHA_TOKEN
}
},
400
);

const provider = new PhoneAuthProvider(auth);
await expect(
provider.verifyPhoneNumber('+15105550000', v2Verifier)
).to.be.rejectedWith(FirebaseError, 'auth/invalid-recaptcha-token');
expect(failureMock.calls[0].request).to.eql({
phoneNumber: '+15105550000',
captchaResponse: FAKE_TOKEN,
clientType: RecaptchaClientType.WEB,
recaptchaToken: 'verification-code',
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});

it('throws argument-error when recaptcha enterprise is enforced, but initializeRecaptchaConfig was not called', async () => {
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
);
// Not call initializeRecaptchaConfig

const provider = new PhoneAuthProvider(auth);
await expect(
provider.verifyPhoneNumber('+15105550000')
).to.be.rejectedWith(FirebaseError, 'auth/argument-error');
});

it('does recaptcha v2 verification when recaptcha enterprise is disabled, but initializeRecaptchaConfig was not called', async () => {
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
);
// Not call initializeRecaptchaConfig

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
});

const provider = new PhoneAuthProvider(auth);
const result = await provider.verifyPhoneNumber(
'+15105550000',
v2Verifier
);

expect(result).to.eq('verification-id');
expect(route.calls[0].request).to.eql({
phoneNumber: '+15105550000',
recaptchaToken: 'verification-code',
captchaResponse: FAKE_TOKEN,
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});

it('does recaptcha v2 verification when recaptcha enterprise is audit, but initializeRecaptchaConfig was not called', async () => {
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
},
recaptchaConfigResponseAudit
);
// Not call initializeRecaptchaConfig

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
});

const provider = new PhoneAuthProvider(auth);
const result = await provider.verifyPhoneNumber(
'+15105550000',
v2Verifier
);

expect(result).to.eq('verification-id');
expect(route.calls[0].request).to.eql({
phoneNumber: '+15105550000',
recaptchaToken: 'verification-code',
captchaResponse: FAKE_TOKEN,
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});

it('throws argument-error when recaptcha enterprise is audit, but initializeRecaptchaConfig was not called', async () => {
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
},
recaptchaConfigResponseAudit
);
// Not call initializeRecaptchaConfig

const provider = new PhoneAuthProvider(auth);
await expect(
provider.verifyPhoneNumber('+15105550000')
).to.be.rejectedWith(FirebaseError, 'auth/argument-error');
});
});

context('.credential', () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/auth/src/platform_browser/providers/phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export class PhoneAuthProvider {
*
* @param phoneInfoOptions - The user's {@link PhoneInfoOptions}. The phone number should be in
* E.164 format (e.g. +16505550101).
* @param applicationVerifier - For abuse prevention, this method also requires a
* {@link ApplicationVerifier}. This SDK includes a reCAPTCHA-based implementation,
* {@link RecaptchaVerifier}.
* @param applicationVerifier - For abuse prevention with reCAPTCHA v2, this method also requires
* a {@link ApplicationVerifier}. This SDK includes a reCAPTCHA-based implementation,
* {@link RecaptchaVerifier}. For abuse prevention with reCAPTCHA Enterprise, {@link ApplicationVerifier}
* is not required, depending on the enforcement state. However, {@link initializeRecaptchaConfig}
* must be called once before initiating reCAPTCHA Enterprise verification.
*
* @returns A Promise for a verification ID that can be passed to
* {@link PhoneAuthProvider.credential} to identify this flow.
Expand Down
Loading

0 comments on commit 7bbb1b2

Please sign in to comment.