Skip to content

Commit d40ca7d

Browse files
authored
Implement validatePassword endpoint and fix PasswordPolicy type to match API proposal (#7456)
* Implement validatePassword * Fix allowedNonAlphanumericCharacters typing to match API proposal
1 parent 97f46f5 commit d40ca7d

File tree

8 files changed

+443
-48
lines changed

8 files changed

+443
-48
lines changed

common/api-review/auth.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ export interface ParsedToken {
564564

565565
// @public
566566
export interface PasswordPolicy {
567-
readonly allowedNonAlphanumericCharacters: string[];
567+
readonly allowedNonAlphanumericCharacters: string;
568568
readonly customStrengthOptions: {
569569
readonly minPasswordLength?: number;
570570
readonly maxPasswordLength?: number;

docs-devsite/auth.passwordpolicy.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface PasswordPolicy
2222

2323
| Property | Type | Description |
2424
| --- | --- | --- |
25-
| [allowedNonAlphanumericCharacters](./auth.passwordpolicy.md#passwordpolicyallowednonalphanumericcharacters) | string\[\] | List of characters that are considered non-alphanumeric during validation. |
25+
| [allowedNonAlphanumericCharacters](./auth.passwordpolicy.md#passwordpolicyallowednonalphanumericcharacters) | string | List of characters that are considered non-alphanumeric during validation. |
2626
| [customStrengthOptions](./auth.passwordpolicy.md#passwordpolicycustomstrengthoptions) | { readonly minPasswordLength?: number; readonly maxPasswordLength?: number; readonly containsLowercaseLetter?: boolean; readonly containsUppercaseLetter?: boolean; readonly containsNumericCharacter?: boolean; readonly containsNonAlphanumericCharacter?: boolean; } | Requirements enforced by this password policy. |
2727

2828
## PasswordPolicy.allowedNonAlphanumericCharacters
@@ -32,7 +32,7 @@ List of characters that are considered non-alphanumeric during validation.
3232
<b>Signature:</b>
3333

3434
```typescript
35-
readonly allowedNonAlphanumericCharacters: string[];
35+
readonly allowedNonAlphanumericCharacters: string;
3636
```
3737

3838
## PasswordPolicy.customStrengthOptions

packages/auth/src/core/auth/auth_impl.test.ts

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { mockEndpointWithParams } from '../../../test/helpers/api/helper';
4545
import { Endpoint, RecaptchaClientType, RecaptchaVersion } from '../../api';
4646
import * as mockFetch from '../../../test/helpers/mock_fetch';
4747
import { AuthErrorCode } from '../errors';
48+
import { PasswordValidationStatus } from '../../model/public_types';
49+
import { PasswordPolicyImpl } from './password_policy_impl';
4850

4951
use(sinonChai);
5052
use(chaiAsPromised);
@@ -789,8 +791,11 @@ describe('core/auth/auth_impl', () => {
789791

790792
context('passwordPolicy', () => {
791793
const TEST_ALLOWED_NON_ALPHANUMERIC_CHARS = ['!', '(', ')'];
794+
const TEST_ALLOWED_NON_ALPHANUMERIC_STRING =
795+
TEST_ALLOWED_NON_ALPHANUMERIC_CHARS.join('');
792796
const TEST_MIN_PASSWORD_LENGTH = 6;
793797
const TEST_SCHEMA_VERSION = 1;
798+
const TEST_UNSUPPORTED_SCHEMA_VERSION = 0;
794799
const TEST_TENANT_ID = 'tenant-id';
795800
const TEST_TENANT_ID_UNSUPPORTED_POLICY_VERSION =
796801
'tenant-id-unsupported-policy-version';
@@ -810,17 +815,36 @@ describe('core/auth/auth_impl', () => {
810815
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
811816
schemaVersion: TEST_SCHEMA_VERSION
812817
};
813-
const PASSWORD_POLICY_RESPONSE_UNSUPPORTED_VERSION = {
818+
const PASSWORD_POLICY_RESPONSE_UNSUPPORTED_SCHEMA_VERSION = {
814819
customStrengthOptions: {
815820
maxPasswordLength: TEST_MIN_PASSWORD_LENGTH,
816821
unsupportedPasswordPolicyProperty: 10
817822
},
818823
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
819-
schemaVersion: 0
824+
schemaVersion: TEST_UNSUPPORTED_SCHEMA_VERSION
825+
};
826+
const CACHED_PASSWORD_POLICY = {
827+
customStrengthOptions: {
828+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH
829+
},
830+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_STRING,
831+
schemaVersion: TEST_SCHEMA_VERSION
832+
};
833+
const CACHED_PASSWORD_POLICY_REQUIRE_NUMERIC = {
834+
customStrengthOptions: {
835+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
836+
containsNumericCharacter: true
837+
},
838+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_STRING,
839+
schemaVersion: TEST_SCHEMA_VERSION
840+
};
841+
const PASSWORD_POLICY_UNSUPPORTED_SCHEMA_VERSION = {
842+
customStrengthOptions: {
843+
maxPasswordLength: TEST_MIN_PASSWORD_LENGTH
844+
},
845+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_STRING,
846+
schemaVersion: TEST_UNSUPPORTED_SCHEMA_VERSION
820847
};
821-
const CACHED_PASSWORD_POLICY = PASSWORD_POLICY_RESPONSE;
822-
const CACHED_PASSWORD_POLICY_REQUIRE_NUMERIC =
823-
PASSWORD_POLICY_RESPONSE_REQUIRE_NUMERIC;
824848

825849
beforeEach(async () => {
826850
mockFetch.setUp();
@@ -841,7 +865,7 @@ describe('core/auth/auth_impl', () => {
841865
{
842866
tenantId: TEST_TENANT_ID_UNSUPPORTED_POLICY_VERSION
843867
},
844-
PASSWORD_POLICY_RESPONSE_UNSUPPORTED_VERSION
868+
PASSWORD_POLICY_RESPONSE_UNSUPPORTED_SCHEMA_VERSION
845869
);
846870
});
847871

@@ -885,14 +909,121 @@ describe('core/auth/auth_impl', () => {
885909
expect(auth._getPasswordPolicyInternal()).to.be.undefined;
886910
});
887911

888-
it('password policy should not be set when the schema version is not supported', async () => {
912+
it('password policy should still be set when the schema version is not supported', async () => {
889913
auth = await testAuth();
890914
auth.tenantId = TEST_TENANT_ID_UNSUPPORTED_POLICY_VERSION;
891-
await expect(auth._updatePasswordPolicy()).to.be.rejectedWith(
892-
AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION
915+
await expect(auth._updatePasswordPolicy()).to.be.fulfilled;
916+
917+
expect(auth._getPasswordPolicyInternal()).to.eql(
918+
PASSWORD_POLICY_UNSUPPORTED_SCHEMA_VERSION
893919
);
920+
});
894921

895-
expect(auth._getPasswordPolicyInternal()).to.be.undefined;
922+
context('#validatePassword', () => {
923+
const PASSWORD_POLICY_IMPL = new PasswordPolicyImpl(
924+
PASSWORD_POLICY_RESPONSE
925+
);
926+
const PASSWORD_POLICY_IMPL_REQUIRE_NUMERIC = new PasswordPolicyImpl(
927+
PASSWORD_POLICY_RESPONSE_REQUIRE_NUMERIC
928+
);
929+
const TEST_BASIC_PASSWORD = 'password';
930+
931+
it('password meeting the policy for the project should be considered valid', async () => {
932+
const expectedValidationStatus: PasswordValidationStatus = {
933+
isValid: true,
934+
meetsMinPasswordLength: true,
935+
passwordPolicy: PASSWORD_POLICY_IMPL
936+
};
937+
938+
auth = await testAuth();
939+
const status = await auth.validatePassword(TEST_BASIC_PASSWORD);
940+
expect(status).to.eql(expectedValidationStatus);
941+
});
942+
943+
it('password not meeting the policy for the project should be considered invalid', async () => {
944+
const expectedValidationStatus: PasswordValidationStatus = {
945+
isValid: false,
946+
meetsMinPasswordLength: false,
947+
passwordPolicy: PASSWORD_POLICY_IMPL
948+
};
949+
950+
auth = await testAuth();
951+
const status = await auth.validatePassword('pass');
952+
expect(status).to.eql(expectedValidationStatus);
953+
});
954+
955+
it('password meeting the policy for the tenant should be considered valid', async () => {
956+
const expectedValidationStatus: PasswordValidationStatus = {
957+
isValid: true,
958+
meetsMinPasswordLength: true,
959+
containsNumericCharacter: true,
960+
passwordPolicy: PASSWORD_POLICY_IMPL_REQUIRE_NUMERIC
961+
};
962+
963+
auth = await testAuth();
964+
auth.tenantId = TEST_TENANT_ID;
965+
const status = await auth.validatePassword('passw0rd');
966+
expect(status).to.eql(expectedValidationStatus);
967+
});
968+
969+
it('password not meeting the policy for the tenant should be considered invalid', async () => {
970+
const expectedValidationStatus: PasswordValidationStatus = {
971+
isValid: false,
972+
meetsMinPasswordLength: false,
973+
containsNumericCharacter: false,
974+
passwordPolicy: PASSWORD_POLICY_IMPL_REQUIRE_NUMERIC
975+
};
976+
977+
auth = await testAuth();
978+
auth.tenantId = TEST_TENANT_ID;
979+
const status = await auth.validatePassword('pass');
980+
expect(status).to.eql(expectedValidationStatus);
981+
});
982+
983+
it('should use the password policy associated with the tenant ID when the tenant ID switches', async () => {
984+
let expectedValidationStatus: PasswordValidationStatus = {
985+
isValid: true,
986+
meetsMinPasswordLength: true,
987+
passwordPolicy: PASSWORD_POLICY_IMPL
988+
};
989+
990+
auth = await testAuth();
991+
992+
let status = await auth.validatePassword(TEST_BASIC_PASSWORD);
993+
expect(status).to.eql(expectedValidationStatus);
994+
995+
expectedValidationStatus = {
996+
isValid: false,
997+
meetsMinPasswordLength: true,
998+
containsNumericCharacter: false,
999+
passwordPolicy: PASSWORD_POLICY_IMPL_REQUIRE_NUMERIC
1000+
};
1001+
1002+
auth.tenantId = TEST_TENANT_ID;
1003+
status = await auth.validatePassword(TEST_BASIC_PASSWORD);
1004+
expect(status).to.eql(expectedValidationStatus);
1005+
});
1006+
1007+
it('should throw an error when a password policy with an unsupported schema version is received', async () => {
1008+
auth = await testAuth();
1009+
auth.tenantId = TEST_TENANT_ID_UNSUPPORTED_POLICY_VERSION;
1010+
await expect(
1011+
auth.validatePassword(TEST_BASIC_PASSWORD)
1012+
).to.be.rejectedWith(
1013+
AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION
1014+
);
1015+
});
1016+
1017+
it('should throw an error when a password policy with an unsupported schema version is already cached', async () => {
1018+
auth = await testAuth();
1019+
auth.tenantId = TEST_TENANT_ID_UNSUPPORTED_POLICY_VERSION;
1020+
await auth._updatePasswordPolicy();
1021+
await expect(
1022+
auth.validatePassword(TEST_BASIC_PASSWORD)
1023+
).to.be.rejectedWith(
1024+
AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION
1025+
);
1026+
});
8961027
});
8971028
});
8981029
});

packages/auth/src/core/auth/auth_impl.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -434,24 +434,15 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
434434
await this._updatePasswordPolicy();
435435
}
436436

437-
return this._getPasswordPolicyInternal()!.validatePassword(password);
438-
}
439-
440-
_getPasswordPolicyInternal(): PasswordPolicyInternal | null {
441-
if (this.tenantId === null) {
442-
return this._projectPasswordPolicy;
443-
} else {
444-
return this._tenantPasswordPolicies[this.tenantId];
445-
}
446-
}
447-
448-
async _updatePasswordPolicy(): Promise<void> {
449-
const response = await _getPasswordPolicy(this);
437+
// Password policy will be defined after fetching.
438+
const passwordPolicy: PasswordPolicyInternal =
439+
this._getPasswordPolicyInternal()!;
450440

451441
// Check that the policy schema version is supported by the SDK.
452442
// TODO: Update this logic to use a max supported policy schema version once we have multiple schema versions.
453443
if (
454-
response.schemaVersion !== this.EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION
444+
passwordPolicy.schemaVersion !==
445+
this.EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION
455446
) {
456447
return Promise.reject(
457448
this._errorFactory.create(
@@ -461,6 +452,20 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
461452
);
462453
}
463454

455+
return passwordPolicy.validatePassword(password);
456+
}
457+
458+
_getPasswordPolicyInternal(): PasswordPolicyInternal | null {
459+
if (this.tenantId === null) {
460+
return this._projectPasswordPolicy;
461+
} else {
462+
return this._tenantPasswordPolicies[this.tenantId];
463+
}
464+
}
465+
466+
async _updatePasswordPolicy(): Promise<void> {
467+
const response = await _getPasswordPolicy(this);
468+
464469
const passwordPolicy: PasswordPolicyInternal = new PasswordPolicyImpl(
465470
response
466471
);

0 commit comments

Comments
 (0)