Skip to content

Commit 1ec33f5

Browse files
feat(auth): validatePassword method implementation (#8400)
* feat(auth): validatePassword method implementation * feat(auth): more definitions to support validatePassword and definition test passes * feat(auth): Shift new definitions into Modular API * feat(auth): implementation draft * feat(auth): Add some internal methods for api call * feat: More API features and fixed errors in tests * feat: license agreement * feat: passwordpolicy api works * feat: Added more tests for password policy API * fix: Code cleanup * fix: more code clean up and documentation * feat: coverage tests for password policy impl * fix: code cleanup * fix: validatePassword fully working * chore: cleanup move api tests to e2e * feat: started adding e2e tests * feat: e2e tests * fix: e2e tests working * fix: formatting * fix: removed redundant comments * fix: formatting * fix: better error messages and fix formatting * fix: cleanup and test fixes * fix: tests * fix: formatting * fix: formatting * fix: top level suite formatting issue * fix: unused var issue * fix: fix last test * fix: last test fix * fix: allow final test to pass on macOS by looking for subtstring * test(auth): re-structure auth unit test file - cleanly split between namespace / modular - some sections were mixed - remove empty block avoiding test - fix double-nesting on ActionCodeSettings * fix: use modular references only * docs: validatePassword documentation * fix: rid trailing spaces --------- Co-authored-by: Mike Hardy <github@mikehardy.net> Co-authored by: Chase Hartsell <51487099+ch5zzy@users.noreply.github.com>
1 parent b000abe commit 1ec33f5

File tree

6 files changed

+521
-86
lines changed

6 files changed

+521
-86
lines changed

packages/auth/__tests__/auth.test.ts

Lines changed: 157 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import auth, {
6161
verifyBeforeUpdateEmail,
6262
getAdditionalUserInfo,
6363
getCustomAuthDomain,
64+
validatePassword,
6465
AppleAuthProvider,
6566
EmailAuthProvider,
6667
FacebookAuthProvider,
@@ -72,6 +73,8 @@ import auth, {
7273
TwitterAuthProvider,
7374
} from '../lib';
7475

76+
const PasswordPolicyImpl = require('../lib/password-policy/PasswordPolicyImpl').default;
77+
7578
// @ts-ignore test
7679
import FirebaseModule from '../../app/lib/internal/FirebaseModule';
7780
// @ts-ignore - We don't mind missing types here
@@ -133,13 +136,13 @@ describe('Auth', function () {
133136
const result = auth().useEmulator('http://my-host:9099');
134137
expect(result).toEqual(['my-host', 9099]);
135138
});
136-
});
137139

138-
describe('tenantId', function () {
139-
it('should be able to set tenantId ', function () {
140-
const auth = firebase.app().auth();
141-
auth.setTenantId('test-id').then(() => {
142-
expect(auth.tenantId).toBe('test-id');
140+
describe('tenantId', function () {
141+
it('should be able to set tenantId ', function () {
142+
const auth = firebase.app().auth();
143+
auth.setTenantId('test-id').then(() => {
144+
expect(auth.tenantId).toBe('test-id');
145+
});
143146
});
144147
});
145148

@@ -201,6 +204,84 @@ describe('Auth', function () {
201204
expect(actual._auth).not.toBeNull();
202205
});
203206
});
207+
208+
describe('ActionCodeSettings', function () {
209+
beforeAll(function () {
210+
// @ts-ignore test
211+
jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => {
212+
return new Proxy(
213+
{},
214+
{
215+
get: () => jest.fn().mockResolvedValue({} as never),
216+
},
217+
);
218+
});
219+
});
220+
221+
it('should allow linkDomain as `ActionCodeSettings.linkDomain`', function () {
222+
const auth = firebase.app().auth();
223+
const actionCodeSettings: FirebaseAuthTypes.ActionCodeSettings = {
224+
url: 'https://example.com',
225+
handleCodeInApp: true,
226+
linkDomain: 'example.com',
227+
};
228+
const email = 'fake@example.com';
229+
auth.sendSignInLinkToEmail(email, actionCodeSettings);
230+
auth.sendPasswordResetEmail(email, actionCodeSettings);
231+
sendPasswordResetEmail(auth, email, actionCodeSettings);
232+
sendSignInLinkToEmail(auth, email, actionCodeSettings);
233+
234+
const user: FirebaseAuthTypes.User = new User(auth, {});
235+
236+
user.sendEmailVerification(actionCodeSettings);
237+
user.verifyBeforeUpdateEmail(email, actionCodeSettings);
238+
sendEmailVerification(user, actionCodeSettings);
239+
verifyBeforeUpdateEmail(user, email, actionCodeSettings);
240+
});
241+
242+
it('should warn using `ActionCodeSettings.dynamicLinkDomain`', function () {
243+
const auth = firebase.app().auth();
244+
const actionCodeSettings: FirebaseAuthTypes.ActionCodeSettings = {
245+
url: 'https://example.com',
246+
handleCodeInApp: true,
247+
linkDomain: 'example.com',
248+
dynamicLinkDomain: 'example.com',
249+
};
250+
const email = 'fake@example.com';
251+
let warnings = 0;
252+
const consoleWarnSpy = jest.spyOn(console, 'warn');
253+
consoleWarnSpy.mockReset();
254+
consoleWarnSpy.mockImplementation(warnMessage => {
255+
if (
256+
warnMessage.includes(
257+
'Instead, use ActionCodeSettings.linkDomain to set up a custom domain',
258+
)
259+
) {
260+
warnings++;
261+
}
262+
});
263+
auth.sendSignInLinkToEmail(email, actionCodeSettings);
264+
expect(warnings).toBe(1);
265+
auth.sendPasswordResetEmail(email, actionCodeSettings);
266+
expect(warnings).toBe(2);
267+
sendPasswordResetEmail(auth, email, actionCodeSettings);
268+
expect(warnings).toBe(3);
269+
sendSignInLinkToEmail(auth, email, actionCodeSettings);
270+
expect(warnings).toBe(4);
271+
const user: FirebaseAuthTypes.User = new User(auth, {});
272+
273+
user.sendEmailVerification(actionCodeSettings);
274+
expect(warnings).toBe(5);
275+
user.verifyBeforeUpdateEmail(email, actionCodeSettings);
276+
expect(warnings).toBe(6);
277+
sendEmailVerification(user, actionCodeSettings);
278+
expect(warnings).toBe(7);
279+
verifyBeforeUpdateEmail(user, email, actionCodeSettings);
280+
expect(warnings).toBe(8);
281+
consoleWarnSpy.mockReset();
282+
consoleWarnSpy.mockRestore();
283+
});
284+
});
204285
});
205286

206287
describe('modular', function () {
@@ -420,6 +501,10 @@ describe('Auth', function () {
420501
expect(getCustomAuthDomain).toBeDefined();
421502
});
422503

504+
it('`validatePassword` function is properly exposed to end user', function () {
505+
expect(validatePassword).toBeDefined();
506+
});
507+
423508
it('`AppleAuthProvider` class is properly exposed to end user', function () {
424509
expect(AppleAuthProvider).toBeDefined();
425510
});
@@ -456,81 +541,79 @@ describe('Auth', function () {
456541
expect(TwitterAuthProvider).toBeDefined();
457542
});
458543

459-
describe('ActionCodeSettings', function () {
460-
beforeAll(function () {
461-
// @ts-ignore test
462-
jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => {
463-
return new Proxy(
464-
{},
465-
{
466-
get: () => jest.fn().mockResolvedValue({} as never),
467-
},
468-
);
469-
});
544+
describe('PasswordPolicyImpl', function () {
545+
const TEST_MIN_PASSWORD_LENGTH = 6;
546+
const TEST_SCHEMA_VERSION = 1;
547+
548+
const testPolicy = {
549+
customStrengthOptions: {
550+
minPasswordLength: 6,
551+
maxPasswordLength: 4096,
552+
containsLowercaseCharacter: true,
553+
containsUppercaseCharacter: true,
554+
containsNumericCharacter: true,
555+
containsNonAlphanumericCharacter: true,
556+
},
557+
allowedNonAlphanumericCharacters: ['$', '*'],
558+
schemaVersion: 1,
559+
enforcementState: 'OFF',
560+
};
561+
562+
it('should create a password policy', async () => {
563+
let passwordPolicy = new PasswordPolicyImpl(testPolicy);
564+
expect(passwordPolicy).toBeDefined();
565+
expect(passwordPolicy.customStrengthOptions.minPasswordLength).toEqual(
566+
TEST_MIN_PASSWORD_LENGTH,
567+
);
568+
expect(passwordPolicy.schemaVersion).toEqual(TEST_SCHEMA_VERSION);
470569
});
471570

472-
it('should allow linkDomain as `ActionCodeSettings.linkDomain`', function () {
473-
const auth = firebase.app().auth();
474-
const actionCodeSettings: FirebaseAuthTypes.ActionCodeSettings = {
475-
url: 'https://example.com',
476-
handleCodeInApp: true,
477-
linkDomain: 'example.com',
478-
};
479-
const email = 'fake@example.com';
480-
auth.sendSignInLinkToEmail(email, actionCodeSettings);
481-
auth.sendPasswordResetEmail(email, actionCodeSettings);
482-
sendPasswordResetEmail(auth, email, actionCodeSettings);
483-
sendSignInLinkToEmail(auth, email, actionCodeSettings);
571+
it('should return statusValid: true when the password satisfies the password policy', async () => {
572+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
573+
let password = 'Password$123';
574+
let status = passwordPolicy.validatePassword(password);
575+
expect(status).toBeDefined();
576+
expect(status.isValid).toEqual(true);
577+
});
484578

485-
const user: FirebaseAuthTypes.User = new User(auth, {});
579+
it('should return statusValid: false when the password is too short', async () => {
580+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
581+
let password = 'Pa1$';
582+
let status = passwordPolicy.validatePassword(password);
583+
expect(status).toBeDefined();
584+
expect(status.isValid).toEqual(false);
585+
});
486586

487-
user.sendEmailVerification(actionCodeSettings);
488-
user.verifyBeforeUpdateEmail(email, actionCodeSettings);
489-
sendEmailVerification(user, actionCodeSettings);
490-
verifyBeforeUpdateEmail(user, email, actionCodeSettings);
587+
it('should return statusValid: false when the password has no capital characters', async () => {
588+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
589+
let password = 'password123$';
590+
let status = passwordPolicy.validatePassword(password);
591+
expect(status).toBeDefined();
592+
expect(status.isValid).toEqual(false);
491593
});
492594

493-
it('should warn using `ActionCodeSettings.dynamicLinkDomain`', function () {
494-
const auth = firebase.app().auth();
495-
const actionCodeSettings: FirebaseAuthTypes.ActionCodeSettings = {
496-
url: 'https://example.com',
497-
handleCodeInApp: true,
498-
linkDomain: 'example.com',
499-
dynamicLinkDomain: 'example.com',
500-
};
501-
const email = 'fake@example.com';
502-
let warnings = 0;
503-
const consoleWarnSpy = jest.spyOn(console, 'warn');
504-
consoleWarnSpy.mockReset();
505-
consoleWarnSpy.mockImplementation(warnMessage => {
506-
if (
507-
warnMessage.includes(
508-
'Instead, use ActionCodeSettings.linkDomain to set up a custom domain',
509-
)
510-
) {
511-
warnings++;
512-
}
513-
});
514-
auth.sendSignInLinkToEmail(email, actionCodeSettings);
515-
expect(warnings).toBe(1);
516-
auth.sendPasswordResetEmail(email, actionCodeSettings);
517-
expect(warnings).toBe(2);
518-
sendPasswordResetEmail(auth, email, actionCodeSettings);
519-
expect(warnings).toBe(3);
520-
sendSignInLinkToEmail(auth, email, actionCodeSettings);
521-
expect(warnings).toBe(4);
522-
const user: FirebaseAuthTypes.User = new User(auth, {});
595+
it('should return statusValid: false when the password has no lowercase characters', async () => {
596+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
597+
let password = 'PASSWORD123$';
598+
let status = passwordPolicy.validatePassword(password);
599+
expect(status).toBeDefined();
600+
expect(status.isValid).toEqual(false);
601+
});
523602

524-
user.sendEmailVerification(actionCodeSettings);
525-
expect(warnings).toBe(5);
526-
user.verifyBeforeUpdateEmail(email, actionCodeSettings);
527-
expect(warnings).toBe(6);
528-
sendEmailVerification(user, actionCodeSettings);
529-
expect(warnings).toBe(7);
530-
verifyBeforeUpdateEmail(user, email, actionCodeSettings);
531-
expect(warnings).toBe(8);
532-
consoleWarnSpy.mockReset();
533-
consoleWarnSpy.mockRestore();
603+
it('should return statusValid: false when the password has no numbers', async () => {
604+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
605+
let password = 'Password$';
606+
let status = passwordPolicy.validatePassword(password);
607+
expect(status).toBeDefined();
608+
expect(status.isValid).toEqual(false);
609+
});
610+
611+
it('should return statusValid: false when the password has no special characters', async () => {
612+
const passwordPolicy = new PasswordPolicyImpl(testPolicy);
613+
let password = 'Password123';
614+
let status = passwordPolicy.validatePassword(password);
615+
expect(status).toBeDefined();
616+
expect(status.isValid).toEqual(false);
534617
});
535618
});
536619
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2016-present Invertase Limited & Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this library except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
import { validatePassword, getAuth } from '../lib/';
19+
20+
describe('auth() -> validatePassword()', function () {
21+
it('isValid is false if password is too short', async function () {
22+
let status = await validatePassword(getAuth(), 'Pa1$');
23+
status.isValid.should.equal(false);
24+
});
25+
26+
it('isValid is false if password is empty string', async function () {
27+
let status = await validatePassword(getAuth(), '');
28+
status.isValid.should.equal(false);
29+
});
30+
31+
it('isValid is false if password has no digits', async function () {
32+
let status = await validatePassword(getAuth(), 'Password$');
33+
status.isValid.should.equal(false);
34+
});
35+
36+
it('isValid is false if password has no capital letters', async function () {
37+
let status = await validatePassword(getAuth(), 'password123$');
38+
status.isValid.should.equal(false);
39+
});
40+
41+
it('isValid is false if password has no lowercase letters', async function () {
42+
let status = await validatePassword(getAuth(), 'PASSWORD123$');
43+
status.isValid.should.equal(false);
44+
});
45+
46+
it('isValid is true if given a password that satisfies the policy', async function () {
47+
let status = await validatePassword(getAuth(), 'Password123$');
48+
status.isValid.should.equal(true);
49+
});
50+
51+
it('validatePassword throws an error if password is null', async function () {
52+
try {
53+
await validatePassword(getAuth(), null);
54+
} catch (e) {
55+
e.message.should.equal(
56+
"firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.",
57+
);
58+
}
59+
});
60+
61+
it('validatePassword throws an error if password is undefined', async function () {
62+
try {
63+
await validatePassword(getAuth(), undefined);
64+
} catch (e) {
65+
e.message.should.equal(
66+
"firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.",
67+
);
68+
}
69+
});
70+
71+
it('validatePassword throws an error if given a bad auth instance', async function () {
72+
try {
73+
await validatePassword(undefined, 'Testing123$');
74+
} catch (e) {
75+
e.message.should.containEql('app');
76+
e.message.should.containEql('undefined');
77+
}
78+
});
79+
});

0 commit comments

Comments
 (0)