Skip to content

Commit

Permalink
Add reCAPTCHA Enterprise support for Phone Auth (#8568)
Browse files Browse the repository at this point in the history
* 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

* Implement reCAPTCHA Enterprise flow for phone provider (#7814)

* 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

* Implement reCAPTCHA Enterprise flow for phone provider

* Cleanup tests

* Make recaptchaEnterpriseVerifier.verify return a mock when appVerificationDisabledForTesting is true

* Lint fix

* yarn docgen devsite

* Mark appVerifier param in Phone Auth APIs as required

* Update API reports

* Change RecaptchaProvider to RecaptchaAuthProvider

* Fix reference docs

* Add more unit tests

---------

Co-authored-by: NhienLam <NhienLam@users.noreply.github.com>

* Make ApplicationVerifier params optional in Phone Auth APIs (#8366)

* Make ApplicationVerifier params optional in Phone APIs

* Add more unit tests for when ApplicationVerifier is not available

* Only fetch reCAPTCHA v2 token when FAKE_TOKEN (#8493)

* Proceed to reCAPTCHA v2 if unable to get reCAPTCHA Enterprise enablement state (#8500)

* Proceed to reCAPTCHA v2 if cannot get phone enablement state

* nit: Add a missing period.

* Add integration tests for rCE ENFORCE (#8538)

* Add integration test for rCE ENFORCE

* format

* Add changeset and refdocs

* Bump main firebase package in changeset

* Addressed refdocs feedback

---------

Co-authored-by: NhienLam <NhienLam@users.noreply.github.com>
  • Loading branch information
NhienLam and NhienLam authored Oct 16, 2024
1 parent 8fb044e commit b942e9e
Show file tree
Hide file tree
Showing 28 changed files with 1,948 additions and 223 deletions.
6 changes: 6 additions & 0 deletions .changeset/shy-bikes-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/auth': minor
'firebase': minor
---

[feature] Added reCAPTCHA Enterprise support for app verification during phone authentication.
8 changes: 4 additions & 4 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export function isSignInWithEmailLink(auth: Auth, emailLink: string): boolean;
export function linkWithCredential(user: User, credential: AuthCredential): Promise<UserCredential>;

// @public
export function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;

// @public
export function linkWithPopup(user: User, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise<UserCredential>;
Expand Down Expand Up @@ -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<string>;
verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier?: ApplicationVerifier): Promise<string>;
}

// @public
Expand Down Expand Up @@ -692,7 +692,7 @@ export interface ReactNativeAsyncStorage {
export function reauthenticateWithCredential(user: User, credential: AuthCredential): Promise<UserCredential>;

// @public
export function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;

// @public
export function reauthenticateWithPopup(user: User, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise<UserCredential>;
Expand Down Expand Up @@ -778,7 +778,7 @@ export function signInWithEmailAndPassword(auth: Auth, email: string, password:
export function signInWithEmailLink(auth: Auth, email: string, emailLink?: string): Promise<UserCredential>;

// @public
export function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;

// @public
export function signInWithPopup(auth: Auth, provider: AuthProvider, resolver?: PopupRedirectResolver): Promise<UserCredential>;
Expand Down
10 changes: 6 additions & 4 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,14 +923,16 @@ 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, this method requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes an implementation based on reCAPTCHA v2, [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.

If you've enabled project-level reCAPTCHA Enterprise bot protection in Enforce mode, you can omit the [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->.

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)<!-- -->.

<b>Signature:</b>

```typescript
export declare function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export declare function signInWithPhoneNumber(auth: Auth, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;
```

#### Parameters
Expand Down Expand Up @@ -1304,7 +1306,7 @@ This method does not work in a Node.js environment.
<b>Signature:</b>

```typescript
export declare function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export declare function linkWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;
```

#### Parameters
Expand Down Expand Up @@ -1457,7 +1459,7 @@ This method does not work in a Node.js environment or on any [User](./auth.user.
<b>Signature:</b>

```typescript
export declare function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier: ApplicationVerifier): Promise<ConfirmationResult>;
export declare function reauthenticateWithPhoneNumber(user: User, phoneNumber: string, appVerifier?: ApplicationVerifier): Promise<ConfirmationResult>;
```

#### Parameters
Expand Down
6 changes: 3 additions & 3 deletions docs-devsite/auth.phoneauthprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,21 @@ Starts a phone number authentication flow by sending a verification code to the
<b>Signature:</b>

```typescript
verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier: ApplicationVerifier): Promise<string>;
verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier?: ApplicationVerifier): Promise<string>;
```

#### Parameters

| 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) | An [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->, which prevents requests from unauthorized clients. This SDK includes an implementation based on reCAPTCHA v2, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. If you've enabled reCAPTCHA Enterprise bot protection in Enforce mode, this parameter is optional; in all other configurations, the parameter is required. |

<b>Returns:</b>

Promise&lt;string&gt;

A Promise for a verification ID that can be passed to [PhoneAuthProvider.credential()](./auth.phoneauthprovider.md#phoneauthprovidercredential) to identify this flow..
A Promise for a verification ID that can be passed to [PhoneAuthProvider.credential()](./auth.phoneauthprovider.md#phoneauthprovidercredential) to identify this flow.

### Example 1

Expand Down
33 changes: 32 additions & 1 deletion packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ firebase emulators:exec --project foo-bar --only auth "yarn test:integration:loc

### Integration testing with the production backend

Currently, MFA TOTP and password policy tests only run against the production backend (since they are not supported on the emulator yet).
Currently, MFA TOTP, password policy, and reCAPTCHA Enterprise phone verification tests only run
against the production backend (since they are not supported on the emulator yet).
Running against the backend also makes it a more reliable end-to-end test.

#### TOTP

The TOTP tests require the following email/password combination to exist in the project, so if you are running this test against your test project, please create this user:

'totpuser-donotdelete@test.com', 'password'
Expand All @@ -71,6 +74,8 @@ curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Conten
}'
```

#### Password policy

The password policy tests require a tenant configured with a password policy that requires all options to exist in the project.

If you are running this test against your test project, please create the tenant and configure the policy with the following curl command:
Expand Down Expand Up @@ -98,6 +103,32 @@ curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Conten

Replace the tenant ID `passpol-tenant-d7hha` in [test/integration/flows/password_policy.test.ts](https://github.com/firebase/firebase-js-sdk/blob/main/packages/auth/test/integration/flows/password_policy.test.ts) with the ID for the newly created tenant. The tenant ID can be found at the end of the `name` property in the response and is in the format `passpol-tenant-xxxxx`.

#### reCAPTCHA Enterprise phone verification

The reCAPTCHA Enterprise phone verification tests require reCAPTCHA Enterprise to be enabled and
the following fictional phone number to be configured and in the project.

If you are running this
test against your project, please [add this test phone number](https://firebase.google.com/docs/auth/web/phone-auth#create-fictional-phone-numbers-and-verification-codes):

'+1 555-555-1000', SMS code: '123456'

Follow [this guide](https://cloud.google.com/identity-platform/docs/recaptcha-enterprise) to enable reCAPTCHA
Enterprise, then use the following curl command to set reCAPTCHA Enterprise to ENFORCE for phone provider:

```
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -H "X-Goog-User-Project: $
{PROJECT_ID}" -X POST https://identitytoolkit.googleapis.com/v2/projects/${PROJECT_ID}/config?updateMask=recaptchaConfig.phoneEnforcementState,recaptchaConfig.useSmsBotScore,recaptchaConfig.useSmsTollFraudProtection -d '
{
"name": "projects/{PROJECT_ID}",
"recaptchaConfig": {
"phoneEnforcementState": "ENFORCE",
"useSmsBotScore": "true",
"useSmsTollFraudProtection": "true",
},
}'
```

### Selenium Webdriver tests

These tests assume that you have both Firefox and Chrome installed on your
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ function getTestFiles(argv) {
if (argv.prodbackend) {
return [
'test/integration/flows/totp.test.ts',
'test/integration/flows/password_policy.test.ts'
'test/integration/flows/password_policy.test.ts',
'test/integration/flows/recaptcha_enterprise.test.ts'
];
}
return argv.local
Expand Down
12 changes: 10 additions & 2 deletions packages/auth/src/api/account_management/mfa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
}
};

Expand Down
9 changes: 8 additions & 1 deletion packages/auth/src/api/account_management/mfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import {
Endpoint,
HttpMethod,
RecaptchaClientType,
RecaptchaVersion,
_addTidIfNecessary,
_performApiRequest
} from '../index';
Expand Down Expand Up @@ -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;
}
Expand Down
12 changes: 10 additions & 2 deletions packages/auth/src/api/authentication/mfa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,7 +39,10 @@ describe('api/authentication/startSignInPhoneMfa', () => {
mfaPendingCredential: 'my-creds',
mfaEnrollmentId: 'my-enrollment-id',
phoneSignInInfo: {
recaptchaToken: 'captcha-token'
recaptchaToken: 'captcha-token',
captchaResponse: 'captcha-response',
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
}
};

Expand Down
9 changes: 8 additions & 1 deletion packages/auth/src/api/authentication/mfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
_performApiRequest,
Endpoint,
HttpMethod,
RecaptchaClientType,
RecaptchaVersion,
_addTidIfNecessary
} from '../index';
import { Auth } from '../../model/public_types';
Expand Down Expand Up @@ -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;
}
Expand Down
12 changes: 10 additions & 2 deletions packages/auth/src/api/authentication/sms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion packages/auth/src/api/authentication/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import {
Endpoint,
HttpMethod,
RecaptchaClientType,
RecaptchaVersion,
_addTidIfNecessary,
_makeTaggedError,
_performApiRequest,
Expand All @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -97,8 +100,9 @@ export const enum EnforcementState {
}

// Providers that have reCAPTCHA Enterprise support.
export const enum RecaptchaProvider {
EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER'
export const enum RecaptchaAuthProvider {
EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER',
PHONE_PROVIDER = 'PHONE_PROVIDER'
}

export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000);
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/core/credentials/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('core/credentials/email', () => {

beforeEach(async () => {
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
});

context('email & password', () => {
Expand Down
12 changes: 9 additions & 3 deletions packages/auth/src/core/credentials/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import { AuthErrorCode } from '../errors';
import { _fail } from '../util/assert';
import { AuthCredential } from './auth_credential';
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
import {
RecaptchaActionName,
RecaptchaClientType,
RecaptchaAuthProvider
} from '../../api';
import { SignUpRequest } from '../../api/authentication/sign_up';
/**
* Interface that represents the credentials returned by {@link EmailAuthProvider} for
Expand Down Expand Up @@ -128,7 +132,8 @@ export class EmailAuthCredential extends AuthCredential {
auth,
request,
RecaptchaActionName.SIGN_IN_WITH_PASSWORD,
signInWithPassword
signInWithPassword,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
case SignInMethod.EMAIL_LINK:
return signInWithEmailLink(auth, {
Expand Down Expand Up @@ -158,7 +163,8 @@ export class EmailAuthCredential extends AuthCredential {
auth,
request,
RecaptchaActionName.SIGN_UP_PASSWORD,
linkEmailPassword
linkEmailPassword,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
case SignInMethod.EMAIL_LINK:
return signInWithEmailLinkForLinking(auth, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('core/strategies/sendPasswordResetEmail', () => {

beforeEach(async () => {
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
mockFetch.setUp();
});

Expand Down
Loading

0 comments on commit b942e9e

Please sign in to comment.