Skip to content

Add reCAPTCHA Enterprise support for Phone Auth #8568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 16, 2024
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 with reCAPTCHA v2, this method 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 required in Audit mode but not in Enforce mode.

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) | For abuse prevention with reCAPTCHA v2, this method requires a [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface)<!-- -->. This SDK includes a reCAPTCHA-v2-based implementation, [RecaptchaVerifier](./auth.recaptchaverifier.md#recaptchaverifier_class)<!-- -->. For abuse prevention with reCAPTCHA Enterprise, [ApplicationVerifier](./auth.applicationverifier.md#applicationverifier_interface) is required in Audit mode but not in Enforce mode. |

<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:

'[email protected]', '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
Loading