Skip to content
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

Add integration tests for rCE ENFORCE #8538

Merged
merged 2 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
198 changes: 198 additions & 0 deletions packages/auth/test/integration/flows/recaptcha_enterprise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* @license
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinonChai from 'sinon-chai';
import {
linkWithPhoneNumber,
PhoneAuthProvider,
reauthenticateWithPhoneNumber,
signInAnonymously,
signInWithPhoneNumber,
unlink,
updatePhoneNumber,
Auth,
OperationType,
ProviderId
} from '@firebase/auth';
import {
cleanUpTestInstance,
getTestInstance
} from '../../helpers/integration/helpers';

import { getEmulatorUrl } from '../../helpers/integration/settings';

use(chaiAsPromised);
use(sinonChai);

let auth: Auth;
let emulatorUrl: string | null;

// NOTE: These happy test cases don't use a real phone number. In order to run these tests
// you must allowlist the following phone number as "testing" numbers in the Auth console.
// https://console.firebase.google.com/u/0/project/_/authentication/providers
// • +1 (555) 555-1000, SMS code 123456

const FICTIONAL_PHONE = {
phoneNumber: '+15555551000',
code: '123456'
};

// This phone number is not allowlisted. It is used in error test cases to catch errors, as
// using fictional phone number always receives success response from the server.
// Note: Don't use this for happy cases because we want to avoid sending actual SMS message.
const NONFICTIONAL_PHONE = {
phoneNumber: '+15555553000'
};

// These tests are written when reCAPTCHA Enterprise is set to ENFORCE. In order to run these tests
// you must enable reCAPTCHA Enterprise in Cloud Console and set enforcement state for PHONE_PROVIDER
// to ENFORCE.
// The CI project has reCAPTCHA bot-score and toll fraud protection enabled.
describe('Integration test: phone auth with reCAPTCHA Enterprise ENFORCE mode', () => {
beforeEach(() => {
emulatorUrl = getEmulatorUrl();
if (!emulatorUrl) {
auth = getTestInstance();
// Sets to false to generate the real reCAPTCHA Enterprise token
auth.settings.appVerificationDisabledForTesting = false;
}
});

afterEach(async () => {
if (!emulatorUrl) {
await cleanUpTestInstance(auth);
}
});

it('allows user to sign in with phone number', async function () {
if (emulatorUrl) {
this.skip();
}

// This generates real recaptcha token and use it for verification
const confirmationResult = await signInWithPhoneNumber(
auth,
FICTIONAL_PHONE.phoneNumber
);
expect(confirmationResult.verificationId).not.to.be.null;

const userCred = await confirmationResult.confirm('123456');
expect(auth.currentUser).to.eq(userCred.user);
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);

const user = userCred.user;
expect(user.isAnonymous).to.be.false;
expect(user.uid).to.be.a('string');
expect(user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);
});

it('throws error if recaptcha token is invalid', async function () {
if (emulatorUrl) {
this.skip();
}
// Simulates a fake token by setting this to true
auth.settings.appVerificationDisabledForTesting = true;

// Use unallowlisted phone number to trigger real reCAPTCHA Enterprise verification
// Since it will throw an error, no SMS will be sent.
await expect(
signInWithPhoneNumber(auth, NONFICTIONAL_PHONE.phoneNumber)
).to.be.rejectedWith('auth/invalid-recaptcha-token');
});

it('anonymous users can upgrade using phone number', async function () {
if (emulatorUrl) {
this.skip();
}
const { user } = await signInAnonymously(auth);
const { uid: anonId } = user;

const provider = new PhoneAuthProvider(auth);
const verificationId = await provider.verifyPhoneNumber(
FICTIONAL_PHONE.phoneNumber
);

await updatePhoneNumber(
user,
PhoneAuthProvider.credential(verificationId, FICTIONAL_PHONE.code)
);
expect(user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);

await auth.signOut();

const cr = await signInWithPhoneNumber(auth, FICTIONAL_PHONE.phoneNumber);
const { user: secondSignIn } = await cr.confirm(FICTIONAL_PHONE.code);

expect(secondSignIn.uid).to.eq(anonId);
expect(secondSignIn.isAnonymous).to.be.false;
expect(secondSignIn.providerData[0].phoneNumber).to.eq(
FICTIONAL_PHONE.phoneNumber
);
expect(secondSignIn.providerData[0].providerId).to.eq('phone');
});

it('anonymous users can link (and unlink) phone number', async function () {
if (emulatorUrl) {
this.skip();
}
const { user } = await signInAnonymously(auth);
const { uid: anonId } = user;

const confirmationResult = await linkWithPhoneNumber(
user,
FICTIONAL_PHONE.phoneNumber
);
const linkResult = await confirmationResult.confirm(FICTIONAL_PHONE.code);
expect(linkResult.operationType).to.eq(OperationType.LINK);
expect(linkResult.user.uid).to.eq(user.uid);
expect(linkResult.user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);

await unlink(user, ProviderId.PHONE);
expect(auth.currentUser!.uid).to.eq(anonId);
// Is anonymous stays false even after unlinking
expect(auth.currentUser!.isAnonymous).to.be.false;
expect(auth.currentUser!.phoneNumber).to.be.null;
});

it('allows the user to reauthenticate with phone number', async function () {
if (emulatorUrl) {
this.skip();
}
// Create a phone user first
let confirmationResult = await signInWithPhoneNumber(
auth,
FICTIONAL_PHONE.phoneNumber
);
const { user } = await confirmationResult.confirm(FICTIONAL_PHONE.code);
const oldToken = await user.getIdToken();

// Wait a bit to ensure the sign in time is different in the token
await new Promise((resolve): void => {
setTimeout(resolve, 1500);
});

confirmationResult = await reauthenticateWithPhoneNumber(
user,
FICTIONAL_PHONE.phoneNumber
);
await confirmationResult.confirm(FICTIONAL_PHONE.code);

expect(await user.getIdToken()).not.to.eq(oldToken);
});
});
Loading