Skip to content

Commit 642259b

Browse files
authored
Add password policy integration tests (#7489)
* Add password policy integration tests and helper method for generating valid passwords * Update prodbackend tests to include password policy tests * Update README with password policy integration testing instructions
1 parent 137b0b7 commit 642259b

File tree

7 files changed

+224
-35
lines changed

7 files changed

+224
-35
lines changed

packages/auth/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ 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 tests only run against the production backend (since they are not supported on the emulator yet).
57+
Currently, MFA TOTP and password policy tests only run against the production backend (since they are not supported on the emulator yet).
5858
Running against the backend also makes it a more reliable end-to-end test.
5959

6060
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:
@@ -71,6 +71,33 @@ curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Conten
7171
}'
7272
```
7373

74+
The password policy tests require a tenant configured with a password policy that requires all options to exist in the project.
75+
76+
If you are running this test against your test project, please create the tenant and configure the policy with the following curl command:
77+
78+
```
79+
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}/tenants -d '{
80+
"displayName": "passpol-tenant",
81+
"passwordPolicyConfig": {
82+
"passwordPolicyEnforcementState": "ENFORCE",
83+
"passwordPolicyVersions": [
84+
{
85+
"customStrengthOptions": {
86+
"minPasswordLength": 8,
87+
"maxPasswordLength": 24,
88+
"containsLowercaseCharacter": true,
89+
"containsUppercaseCharacter": true,
90+
"containsNumericCharacter": true,
91+
"containsNonAlphanumericCharacter": true
92+
}
93+
}
94+
]
95+
}
96+
}'
97+
```
98+
99+
Replace the tenant ID `passpol-tenant-d7hha` in [test/integration/flows/password_policy.test.ts](https://github.com/firebase/firebase-js-sdk/blob/master/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`.
100+
74101
### Selenium Webdriver tests
75102

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

packages/auth/karma.conf.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ function getTestFiles(argv) {
3838
return ['src/**/*.test.ts', 'test/helpers/**/*.test.ts'];
3939
} else if (argv.integration) {
4040
if (argv.prodbackend) {
41-
return ['test/integration/flows/totp.test.ts'];
41+
return [
42+
'test/integration/flows/totp.test.ts',
43+
'test/integration/flows/password_policy.test.ts'
44+
];
4245
}
4346
return argv.local
4447
? ['test/integration/flows/*.test.ts']

packages/auth/test/helpers/integration/helpers.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { getAppConfig, getEmulatorUrl } from './settings';
2525
import { resetEmulator } from './emulator_rest_helpers';
2626
// @ts-ignore - ignore types since this is only used in tests.
2727
import totp from 'totp-generator';
28+
import { _castAuth } from '../../../internal';
2829
interface IntegrationTestAuth extends Auth {
2930
cleanUp(): Promise<void>;
3031
}
@@ -116,3 +117,37 @@ export const email = '[email protected]';
116117
export const fakePassword = 'password';
117118
//1000000 is always incorrect since it has 7 digits and we expect 6.
118119
export const incorrectTotpCode = '1000000';
120+
121+
/**
122+
* Generates a valid password for the project or tenant password policy in the Auth instance.
123+
* @param auth The {@link Auth} instance.
124+
* @returns A valid password according to the password policy.
125+
*/
126+
export async function generateValidPassword(auth: Auth): Promise<string> {
127+
if (getEmulatorUrl()) {
128+
return 'password';
129+
}
130+
131+
// Fetch the policy using the Auth instance if one is not cached.
132+
const authInternal = _castAuth(auth);
133+
if (!authInternal._getPasswordPolicyInternal()) {
134+
await authInternal._updatePasswordPolicy();
135+
}
136+
137+
const passwordPolicy = authInternal._getPasswordPolicyInternal()!;
138+
const options = passwordPolicy.customStrengthOptions;
139+
140+
// Create a string that satisfies all possible options (uppercase, lowercase, numeric, and special characters).
141+
const nonAlphaNumericCharacter =
142+
passwordPolicy.allowedNonAlphanumericCharacters.charAt(0);
143+
const stringWithAllOptions = 'aA0' + nonAlphaNumericCharacter;
144+
145+
// Repeat the string enough times to fill up the minimum password length.
146+
const minPasswordLength = options.minPasswordLength ?? 6;
147+
const password = stringWithAllOptions.repeat(
148+
Math.round(minPasswordLength / stringWithAllOptions.length)
149+
);
150+
151+
// Return a string that is only as long as the minimum length required by the policy.
152+
return password.substring(0, minPasswordLength);
153+
}

packages/auth/test/integration/flows/anonymous.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,21 @@ describe('Integration test: anonymous auth', () => {
6565

6666
context('email/password interaction', () => {
6767
let email: string;
68+
let password: string;
6869

6970
beforeEach(() => {
7071
email = randomEmail();
72+
password = 'password';
73+
// Uncomment the following line if you want an autogenerated password that complies with password policy in the test project.
74+
// password = await generateValidPassword(auth);
7175
});
7276

7377
it('anonymous / email-password accounts remain independent', async () => {
7478
let anonCred = await signInAnonymously(auth);
7579
const emailCred = await createUserWithEmailAndPassword(
7680
auth,
7781
email,
78-
'password'
82+
password
7983
);
8084
expect(emailCred.user.uid).not.to.eql(anonCred.user.uid);
8185

@@ -84,7 +88,7 @@ describe('Integration test: anonymous auth', () => {
8488
const emailSignIn = await signInWithEmailAndPassword(
8589
auth,
8690
email,
87-
'password'
91+
password
8892
);
8993
expect(emailCred.user.uid).to.eql(emailSignIn.user.uid);
9094
expect(emailSignIn.user.uid).not.to.eql(anonCred.user.uid);
@@ -93,36 +97,36 @@ describe('Integration test: anonymous auth', () => {
9397
it('account can be upgraded by setting email and password', async () => {
9498
const { user: anonUser } = await signInAnonymously(auth);
9599
await updateEmail(anonUser, email);
96-
await updatePassword(anonUser, 'password');
100+
await updatePassword(anonUser, password);
97101

98102
await auth.signOut();
99103

100104
const { user: emailPassUser } = await signInWithEmailAndPassword(
101105
auth,
102106
email,
103-
'password'
107+
password
104108
);
105109
expect(emailPassUser.uid).to.eq(anonUser.uid);
106110
});
107111

108112
it('account can be linked using email and password', async () => {
109113
const { user: anonUser } = await signInAnonymously(auth);
110-
const cred = EmailAuthProvider.credential(email, 'password');
114+
const cred = EmailAuthProvider.credential(email, password);
111115
await linkWithCredential(anonUser, cred);
112116
await auth.signOut();
113117

114118
const { user: emailPassUser } = await signInWithEmailAndPassword(
115119
auth,
116120
email,
117-
'password'
121+
password
118122
);
119123
expect(emailPassUser.uid).to.eq(anonUser.uid);
120124
});
121125

122126
it('account cannot be linked with existing email/password', async () => {
123-
await createUserWithEmailAndPassword(auth, email, 'password');
127+
await createUserWithEmailAndPassword(auth, email, password);
124128
const { user: anonUser } = await signInAnonymously(auth);
125-
const cred = EmailAuthProvider.credential(email, 'password');
129+
const cred = EmailAuthProvider.credential(email, password);
126130
await expect(linkWithCredential(anonUser, cred)).to.be.rejectedWith(
127131
FirebaseError,
128132
'auth/email-already-in-use'

packages/auth/test/integration/flows/email.test.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ use(chaiAsPromised);
4545
describe('Integration test: email/password auth', () => {
4646
let auth: Auth;
4747
let email: string;
48+
let password: string;
49+
4850
beforeEach(() => {
4951
auth = getTestInstance();
5052
email = randomEmail();
53+
password = 'password';
54+
// Uncomment the following line if you want an autogenerated password that complies with password policy in the test project.
55+
// password = await generateValidPassword(auth);
5156
});
5257

5358
afterEach(() => cleanUpTestInstance(auth));
@@ -56,7 +61,7 @@ describe('Integration test: email/password auth', () => {
5661
const userCred = await createUserWithEmailAndPassword(
5762
auth,
5863
email,
59-
'password'
64+
password
6065
);
6166
expect(auth.currentUser).to.eq(userCred.user);
6267
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);
@@ -76,29 +81,25 @@ describe('Integration test: email/password auth', () => {
7681
});
7782

7883
it('errors when createUser called twice', async () => {
79-
await createUserWithEmailAndPassword(auth, email, 'password');
84+
await createUserWithEmailAndPassword(auth, email, password);
8085
await expect(
81-
createUserWithEmailAndPassword(auth, email, 'password')
86+
createUserWithEmailAndPassword(auth, email, password)
8287
).to.be.rejectedWith(FirebaseError, 'auth/email-already-in-use');
8388
});
8489

8590
context('with existing user', () => {
8691
let signUpCred: UserCredential;
8792

8893
beforeEach(async () => {
89-
signUpCred = await createUserWithEmailAndPassword(
90-
auth,
91-
email,
92-
'password'
93-
);
94+
signUpCred = await createUserWithEmailAndPassword(auth, email, password);
9495
await auth.signOut();
9596
});
9697

9798
it('allows the user to sign in with signInWithEmailAndPassword', async () => {
9899
const signInCred = await signInWithEmailAndPassword(
99100
auth,
100101
email,
101-
'password'
102+
password
102103
);
103104
expect(auth.currentUser).to.eq(signInCred.user);
104105

@@ -110,7 +111,7 @@ describe('Integration test: email/password auth', () => {
110111
});
111112

112113
it('allows the user to sign in with signInWithCredential', async () => {
113-
const credential = EmailAuthProvider.credential(email, 'password');
114+
const credential = EmailAuthProvider.credential(email, password);
114115
const signInCred = await signInWithCredential(auth, credential);
115116
expect(auth.currentUser).to.eq(signInCred.user);
116117

@@ -122,7 +123,7 @@ describe('Integration test: email/password auth', () => {
122123
});
123124

124125
it('allows the user to update profile', async () => {
125-
let { user } = await signInWithEmailAndPassword(auth, email, 'password');
126+
let { user } = await signInWithEmailAndPassword(auth, email, password);
126127
await updateProfile(user, {
127128
displayName: 'Display Name',
128129
photoURL: 'photo-url'
@@ -132,17 +133,13 @@ describe('Integration test: email/password auth', () => {
132133

133134
await auth.signOut();
134135

135-
user = (await signInWithEmailAndPassword(auth, email, 'password')).user;
136+
user = (await signInWithEmailAndPassword(auth, email, password)).user;
136137
expect(user.displayName).to.eq('Display Name');
137138
expect(user.photoURL).to.eq('photo-url');
138139
});
139140

140141
it('allows the user to delete the account', async () => {
141-
const { user } = await signInWithEmailAndPassword(
142-
auth,
143-
email,
144-
'password'
145-
);
142+
const { user } = await signInWithEmailAndPassword(auth, email, password);
146143
await user.delete();
147144

148145
await expect(reload(user)).to.be.rejectedWith(
@@ -152,28 +149,28 @@ describe('Integration test: email/password auth', () => {
152149

153150
expect(auth.currentUser).to.be.null;
154151
await expect(
155-
signInWithEmailAndPassword(auth, email, 'password')
152+
signInWithEmailAndPassword(auth, email, password)
156153
).to.be.rejectedWith(FirebaseError, 'auth/user-not-found');
157154
});
158155

159156
it('sign in can be called twice successively', async () => {
160157
const { user: userA } = await signInWithEmailAndPassword(
161158
auth,
162159
email,
163-
'password'
160+
password
164161
);
165162
const { user: userB } = await signInWithEmailAndPassword(
166163
auth,
167164
email,
168-
'password'
165+
password
169166
);
170167
expect(userA.uid).to.eq(userB.uid);
171168
});
172169

173170
generateMiddlewareTests(
174171
() => auth,
175172
() => {
176-
return signInWithEmailAndPassword(auth, email, 'password');
173+
return signInWithEmailAndPassword(auth, email, password);
177174
}
178175
);
179176
});

packages/auth/test/integration/flows/middleware_test_generator.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@ export function generateMiddlewareTests(
3434
context('middleware', () => {
3535
let auth: Auth;
3636
let unsubscribes: Array<() => void>;
37+
let password: string;
3738

3839
beforeEach(() => {
3940
auth = authGetter();
4041
unsubscribes = [];
42+
password = 'password';
43+
// Uncomment the following line if you want an autogenerated password that complies with password policy in the test project.
44+
// password = await generateValidPassword(auth);
4145
});
4246

4347
afterEach(() => {
@@ -81,7 +85,7 @@ export function generateMiddlewareTests(
8185
const { user: baseUser } = await createUserWithEmailAndPassword(
8286
auth,
8387
randomEmail(),
84-
'password'
88+
password
8589
);
8690

8791
beforeAuthStateChanged(() => {
@@ -115,7 +119,7 @@ export function generateMiddlewareTests(
115119
const { user: baseUser } = await createUserWithEmailAndPassword(
116120
auth,
117121
randomEmail(),
118-
'password'
122+
password
119123
);
120124

121125
beforeAuthStateChanged(() => {
@@ -148,7 +152,7 @@ export function generateMiddlewareTests(
148152
const { user: baseUser } = await createUserWithEmailAndPassword(
149153
auth,
150154
randomEmail(),
151-
'password'
155+
password
152156
);
153157

154158
// Also check that the function is called multiple
@@ -172,7 +176,7 @@ export function generateMiddlewareTests(
172176
const { user: baseUser } = await createUserWithEmailAndPassword(
173177
auth,
174178
randomEmail(),
175-
'password'
179+
password
176180
);
177181

178182
// Also check that the function is called multiple

0 commit comments

Comments
 (0)