Skip to content

Commit f7223fc

Browse files
committed
Add integration tests for rCE ENFORCE (#8538)
* Add integration test for rCE ENFORCE * format
1 parent 4aa9462 commit f7223fc

File tree

3 files changed

+232
-2
lines changed

3 files changed

+232
-2
lines changed

packages/auth/README.md

+32-1
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ firebase emulators:exec --project foo-bar --only auth "yarn test:integration:loc
5454

5555
### Integration testing with the production backend
5656

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

61+
#### TOTP
62+
6063
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:
6164

6265
'[email protected]', 'password'
@@ -71,6 +74,8 @@ curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Conten
7174
}'
7275
```
7376

77+
#### Password policy
78+
7479
The password policy tests require a tenant configured with a password policy that requires all options to exist in the project.
7580

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

99104
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`.
100105

106+
#### reCAPTCHA Enterprise phone verification
107+
108+
The reCAPTCHA Enterprise phone verification tests require reCAPTCHA Enterprise to be enabled and
109+
the following fictional phone number to be configured and in the project.
110+
111+
If you are running this
112+
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):
113+
114+
'+1 555-555-1000', SMS code: '123456'
115+
116+
Follow [this guide](https://cloud.google.com/identity-platform/docs/recaptcha-enterprise) to enable reCAPTCHA
117+
Enterprise, then use the following curl command to set reCAPTCHA Enterprise to ENFORCE for phone provider:
118+
119+
```
120+
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -H "X-Goog-User-Project: $
121+
{PROJECT_ID}" -X POST https://identitytoolkit.googleapis.com/v2/projects/${PROJECT_ID}/config?updateMask=recaptchaConfig.phoneEnforcementState,recaptchaConfig.useSmsBotScore,recaptchaConfig.useSmsTollFraudProtection -d '
122+
{
123+
"name": "projects/{PROJECT_ID}",
124+
"recaptchaConfig": {
125+
"phoneEnforcementState": "ENFORCE",
126+
"useSmsBotScore": "true",
127+
"useSmsTollFraudProtection": "true",
128+
},
129+
}'
130+
```
131+
101132
### Selenium Webdriver tests
102133

103134
These tests assume that you have both Firefox and Chrome installed on your

packages/auth/karma.conf.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ function getTestFiles(argv) {
5151
if (argv.prodbackend) {
5252
return [
5353
'test/integration/flows/totp.test.ts',
54-
'test/integration/flows/password_policy.test.ts'
54+
'test/integration/flows/password_policy.test.ts',
55+
'test/integration/flows/recaptcha_enterprise.test.ts'
5556
];
5657
}
5758
return argv.local
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import chaiAsPromised from 'chai-as-promised';
20+
import sinonChai from 'sinon-chai';
21+
import {
22+
linkWithPhoneNumber,
23+
PhoneAuthProvider,
24+
reauthenticateWithPhoneNumber,
25+
signInAnonymously,
26+
signInWithPhoneNumber,
27+
unlink,
28+
updatePhoneNumber,
29+
Auth,
30+
OperationType,
31+
ProviderId
32+
} from '@firebase/auth';
33+
import {
34+
cleanUpTestInstance,
35+
getTestInstance
36+
} from '../../helpers/integration/helpers';
37+
38+
import { getEmulatorUrl } from '../../helpers/integration/settings';
39+
40+
use(chaiAsPromised);
41+
use(sinonChai);
42+
43+
let auth: Auth;
44+
let emulatorUrl: string | null;
45+
46+
// NOTE: These happy test cases don't use a real phone number. In order to run these tests
47+
// you must allowlist the following phone number as "testing" numbers in the Auth console.
48+
// https://console.firebase.google.com/u/0/project/_/authentication/providers
49+
// • +1 (555) 555-1000, SMS code 123456
50+
51+
const FICTIONAL_PHONE = {
52+
phoneNumber: '+15555551000',
53+
code: '123456'
54+
};
55+
56+
// This phone number is not allowlisted. It is used in error test cases to catch errors, as
57+
// using fictional phone number always receives success response from the server.
58+
// Note: Don't use this for happy cases because we want to avoid sending actual SMS message.
59+
const NONFICTIONAL_PHONE = {
60+
phoneNumber: '+15555553000'
61+
};
62+
63+
// These tests are written when reCAPTCHA Enterprise is set to ENFORCE. In order to run these tests
64+
// you must enable reCAPTCHA Enterprise in Cloud Console and set enforcement state for PHONE_PROVIDER
65+
// to ENFORCE.
66+
// The CI project has reCAPTCHA bot-score and toll fraud protection enabled.
67+
describe('Integration test: phone auth with reCAPTCHA Enterprise ENFORCE mode', () => {
68+
beforeEach(() => {
69+
emulatorUrl = getEmulatorUrl();
70+
if (!emulatorUrl) {
71+
auth = getTestInstance();
72+
// Sets to false to generate the real reCAPTCHA Enterprise token
73+
auth.settings.appVerificationDisabledForTesting = false;
74+
}
75+
});
76+
77+
afterEach(async () => {
78+
if (!emulatorUrl) {
79+
await cleanUpTestInstance(auth);
80+
}
81+
});
82+
83+
it('allows user to sign in with phone number', async function () {
84+
if (emulatorUrl) {
85+
this.skip();
86+
}
87+
88+
// This generates real recaptcha token and use it for verification
89+
const confirmationResult = await signInWithPhoneNumber(
90+
auth,
91+
FICTIONAL_PHONE.phoneNumber
92+
);
93+
expect(confirmationResult.verificationId).not.to.be.null;
94+
95+
const userCred = await confirmationResult.confirm('123456');
96+
expect(auth.currentUser).to.eq(userCred.user);
97+
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);
98+
99+
const user = userCred.user;
100+
expect(user.isAnonymous).to.be.false;
101+
expect(user.uid).to.be.a('string');
102+
expect(user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);
103+
});
104+
105+
it('throws error if recaptcha token is invalid', async function () {
106+
if (emulatorUrl) {
107+
this.skip();
108+
}
109+
// Simulates a fake token by setting this to true
110+
auth.settings.appVerificationDisabledForTesting = true;
111+
112+
// Use unallowlisted phone number to trigger real reCAPTCHA Enterprise verification
113+
// Since it will throw an error, no SMS will be sent.
114+
await expect(
115+
signInWithPhoneNumber(auth, NONFICTIONAL_PHONE.phoneNumber)
116+
).to.be.rejectedWith('auth/invalid-recaptcha-token');
117+
});
118+
119+
it('anonymous users can upgrade using phone number', async function () {
120+
if (emulatorUrl) {
121+
this.skip();
122+
}
123+
const { user } = await signInAnonymously(auth);
124+
const { uid: anonId } = user;
125+
126+
const provider = new PhoneAuthProvider(auth);
127+
const verificationId = await provider.verifyPhoneNumber(
128+
FICTIONAL_PHONE.phoneNumber
129+
);
130+
131+
await updatePhoneNumber(
132+
user,
133+
PhoneAuthProvider.credential(verificationId, FICTIONAL_PHONE.code)
134+
);
135+
expect(user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);
136+
137+
await auth.signOut();
138+
139+
const cr = await signInWithPhoneNumber(auth, FICTIONAL_PHONE.phoneNumber);
140+
const { user: secondSignIn } = await cr.confirm(FICTIONAL_PHONE.code);
141+
142+
expect(secondSignIn.uid).to.eq(anonId);
143+
expect(secondSignIn.isAnonymous).to.be.false;
144+
expect(secondSignIn.providerData[0].phoneNumber).to.eq(
145+
FICTIONAL_PHONE.phoneNumber
146+
);
147+
expect(secondSignIn.providerData[0].providerId).to.eq('phone');
148+
});
149+
150+
it('anonymous users can link (and unlink) phone number', async function () {
151+
if (emulatorUrl) {
152+
this.skip();
153+
}
154+
const { user } = await signInAnonymously(auth);
155+
const { uid: anonId } = user;
156+
157+
const confirmationResult = await linkWithPhoneNumber(
158+
user,
159+
FICTIONAL_PHONE.phoneNumber
160+
);
161+
const linkResult = await confirmationResult.confirm(FICTIONAL_PHONE.code);
162+
expect(linkResult.operationType).to.eq(OperationType.LINK);
163+
expect(linkResult.user.uid).to.eq(user.uid);
164+
expect(linkResult.user.phoneNumber).to.eq(FICTIONAL_PHONE.phoneNumber);
165+
166+
await unlink(user, ProviderId.PHONE);
167+
expect(auth.currentUser!.uid).to.eq(anonId);
168+
// Is anonymous stays false even after unlinking
169+
expect(auth.currentUser!.isAnonymous).to.be.false;
170+
expect(auth.currentUser!.phoneNumber).to.be.null;
171+
});
172+
173+
it('allows the user to reauthenticate with phone number', async function () {
174+
if (emulatorUrl) {
175+
this.skip();
176+
}
177+
// Create a phone user first
178+
let confirmationResult = await signInWithPhoneNumber(
179+
auth,
180+
FICTIONAL_PHONE.phoneNumber
181+
);
182+
const { user } = await confirmationResult.confirm(FICTIONAL_PHONE.code);
183+
const oldToken = await user.getIdToken();
184+
185+
// Wait a bit to ensure the sign in time is different in the token
186+
await new Promise((resolve): void => {
187+
setTimeout(resolve, 1500);
188+
});
189+
190+
confirmationResult = await reauthenticateWithPhoneNumber(
191+
user,
192+
FICTIONAL_PHONE.phoneNumber
193+
);
194+
await confirmationResult.confirm(FICTIONAL_PHONE.code);
195+
196+
expect(await user.getIdToken()).not.to.eq(oldToken);
197+
});
198+
});

0 commit comments

Comments
 (0)