Skip to content

Commit 1741fef

Browse files
authored
Implement password policy caching in auth object
* Add PasswordPolicy and PasswordValidationStatus public types * Add public types to docs and implement AuthInternal password policy cache * Add schema version mismatch error and tests * Update error code in fetch test and add TODO for schema version handling
1 parent c622c1f commit 1741fef

File tree

10 files changed

+445
-6
lines changed

10 files changed

+445
-6
lines changed

common/api-review/auth.api.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,31 @@ export interface ParsedToken {
562562
'sub'?: string;
563563
}
564564

565+
// @public
566+
export interface PasswordPolicy {
567+
readonly allowedNonAlphanumericCharacters: string[];
568+
readonly customStrengthOptions: {
569+
readonly minPasswordLength?: number;
570+
readonly maxPasswordLength?: number;
571+
readonly containsLowercaseLetter?: boolean;
572+
readonly containsUppercaseLetter?: boolean;
573+
readonly containsNumericCharacter?: boolean;
574+
readonly containsNonAlphanumericCharacter?: boolean;
575+
};
576+
}
577+
578+
// @public
579+
export interface PasswordValidationStatus {
580+
readonly containsLowercaseLetter?: boolean;
581+
readonly containsNonAlphanumericCharacter?: boolean;
582+
readonly containsNumericCharacter?: boolean;
583+
readonly containsUppercaseLetter?: boolean;
584+
readonly isValid: boolean;
585+
readonly meetsMaxPasswordLength?: boolean;
586+
readonly meetsMinPasswordLength?: boolean;
587+
readonly passwordPolicy: PasswordPolicy;
588+
}
589+
565590
// @public
566591
export interface Persistence {
567592
readonly type: 'SESSION' | 'LOCAL' | 'NONE';

docs-devsite/auth.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ Firebase Authentication
124124
| [MultiFactorUser](./auth.multifactoruser.md#multifactoruser_interface) | An interface that defines the multi-factor related properties and operations pertaining to a [User](./auth.user.md#user_interface)<!-- -->. |
125125
| [OAuthCredentialOptions](./auth.oauthcredentialoptions.md#oauthcredentialoptions_interface) | Defines the options for initializing an [OAuthCredential](./auth.oauthcredential.md#oauthcredential_class)<!-- -->. |
126126
| [ParsedToken](./auth.parsedtoken.md#parsedtoken_interface) | Interface representing a parsed ID token. |
127+
| [PasswordPolicy](./auth.passwordpolicy.md#passwordpolicy_interface) | A structure specifying password policy requirements. |
128+
| [PasswordValidationStatus](./auth.passwordvalidationstatus.md#passwordvalidationstatus_interface) | A structure indicating which password policy requirements were met or violated and what the requirements are. |
127129
| [Persistence](./auth.persistence.md#persistence_interface) | An interface covering the possible persistence mechanism types. |
128130
| [PhoneMultiFactorAssertion](./auth.phonemultifactorassertion.md#phonemultifactorassertion_interface) | The class for asserting ownership of a phone second factor. Provided by [PhoneMultiFactorGenerator.assertion()](./auth.phonemultifactorgenerator.md#phonemultifactorgeneratorassertion)<!-- -->. |
129131
| [PhoneMultiFactorEnrollInfoOptions](./auth.phonemultifactorenrollinfooptions.md#phonemultifactorenrollinfooptions_interface) | Options used for enrolling a second factor. |

docs-devsite/auth.passwordpolicy.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# PasswordPolicy interface
13+
A structure specifying password policy requirements.
14+
15+
<b>Signature:</b>
16+
17+
```typescript
18+
export interface PasswordPolicy
19+
```
20+
21+
## Properties
22+
23+
| Property | Type | Description |
24+
| --- | --- | --- |
25+
| [allowedNonAlphanumericCharacters](./auth.passwordpolicy.md#passwordpolicyallowednonalphanumericcharacters) | string\[\] | List of characters that are considered non-alphanumeric during validation. |
26+
| [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. |
27+
28+
## PasswordPolicy.allowedNonAlphanumericCharacters
29+
30+
List of characters that are considered non-alphanumeric during validation.
31+
32+
<b>Signature:</b>
33+
34+
```typescript
35+
readonly allowedNonAlphanumericCharacters: string[];
36+
```
37+
38+
## PasswordPolicy.customStrengthOptions
39+
40+
Requirements enforced by this password policy.
41+
42+
<b>Signature:</b>
43+
44+
```typescript
45+
readonly customStrengthOptions: {
46+
readonly minPasswordLength?: number;
47+
readonly maxPasswordLength?: number;
48+
readonly containsLowercaseLetter?: boolean;
49+
readonly containsUppercaseLetter?: boolean;
50+
readonly containsNumericCharacter?: boolean;
51+
readonly containsNonAlphanumericCharacter?: boolean;
52+
};
53+
```
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# PasswordValidationStatus interface
13+
A structure indicating which password policy requirements were met or violated and what the requirements are.
14+
15+
<b>Signature:</b>
16+
17+
```typescript
18+
export interface PasswordValidationStatus
19+
```
20+
21+
## Properties
22+
23+
| Property | Type | Description |
24+
| --- | --- | --- |
25+
| [containsLowercaseLetter](./auth.passwordvalidationstatus.md#passwordvalidationstatuscontainslowercaseletter) | boolean | Whether the password contains a lowercase letter, if required. |
26+
| [containsNonAlphanumericCharacter](./auth.passwordvalidationstatus.md#passwordvalidationstatuscontainsnonalphanumericcharacter) | boolean | Whether the password contains a non-alphanumeric character, if required. |
27+
| [containsNumericCharacter](./auth.passwordvalidationstatus.md#passwordvalidationstatuscontainsnumericcharacter) | boolean | Whether the password contains a numeric character, if required. |
28+
| [containsUppercaseLetter](./auth.passwordvalidationstatus.md#passwordvalidationstatuscontainsuppercaseletter) | boolean | Whether the password contains an uppercase letter, if required. |
29+
| [isValid](./auth.passwordvalidationstatus.md#passwordvalidationstatusisvalid) | boolean | Whether the password meets all requirements. |
30+
| [meetsMaxPasswordLength](./auth.passwordvalidationstatus.md#passwordvalidationstatusmeetsmaxpasswordlength) | boolean | Whether the password meets the maximum password length. |
31+
| [meetsMinPasswordLength](./auth.passwordvalidationstatus.md#passwordvalidationstatusmeetsminpasswordlength) | boolean | Whether the password meets the minimum password length. |
32+
| [passwordPolicy](./auth.passwordvalidationstatus.md#passwordvalidationstatuspasswordpolicy) | [PasswordPolicy](./auth.passwordpolicy.md#passwordpolicy_interface) | The policy used to validate the password. |
33+
34+
## PasswordValidationStatus.containsLowercaseLetter
35+
36+
Whether the password contains a lowercase letter, if required.
37+
38+
<b>Signature:</b>
39+
40+
```typescript
41+
readonly containsLowercaseLetter?: boolean;
42+
```
43+
44+
## PasswordValidationStatus.containsNonAlphanumericCharacter
45+
46+
Whether the password contains a non-alphanumeric character, if required.
47+
48+
<b>Signature:</b>
49+
50+
```typescript
51+
readonly containsNonAlphanumericCharacter?: boolean;
52+
```
53+
54+
## PasswordValidationStatus.containsNumericCharacter
55+
56+
Whether the password contains a numeric character, if required.
57+
58+
<b>Signature:</b>
59+
60+
```typescript
61+
readonly containsNumericCharacter?: boolean;
62+
```
63+
64+
## PasswordValidationStatus.containsUppercaseLetter
65+
66+
Whether the password contains an uppercase letter, if required.
67+
68+
<b>Signature:</b>
69+
70+
```typescript
71+
readonly containsUppercaseLetter?: boolean;
72+
```
73+
74+
## PasswordValidationStatus.isValid
75+
76+
Whether the password meets all requirements.
77+
78+
<b>Signature:</b>
79+
80+
```typescript
81+
readonly isValid: boolean;
82+
```
83+
84+
## PasswordValidationStatus.meetsMaxPasswordLength
85+
86+
Whether the password meets the maximum password length.
87+
88+
<b>Signature:</b>
89+
90+
```typescript
91+
readonly meetsMaxPasswordLength?: boolean;
92+
```
93+
94+
## PasswordValidationStatus.meetsMinPasswordLength
95+
96+
Whether the password meets the minimum password length.
97+
98+
<b>Signature:</b>
99+
100+
```typescript
101+
readonly meetsMinPasswordLength?: boolean;
102+
```
103+
104+
## PasswordValidationStatus.passwordPolicy
105+
106+
The policy used to validate the password.
107+
108+
<b>Signature:</b>
109+
110+
```typescript
111+
readonly passwordPolicy: PasswordPolicy;
112+
```

packages/auth/src/api/password_policy/get_password_policy.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ describe('api/password_policy/getPasswordPolicy', () => {
7171
{
7272
error: {
7373
code: 400,
74-
message: ServerError.INVALID_PROVIDER_ID,
74+
message: ServerError.TOO_MANY_ATTEMPTS_TRY_LATER,
7575
errors: [
7676
{
77-
message: ServerError.INVALID_PROVIDER_ID
77+
message: ServerError.TOO_MANY_ATTEMPTS_TRY_LATER
7878
}
7979
]
8080
}
@@ -84,7 +84,7 @@ describe('api/password_policy/getPasswordPolicy', () => {
8484

8585
await expect(_getPasswordPolicy(auth)).to.be.rejectedWith(
8686
FirebaseError,
87-
'Firebase: The specified provider ID is invalid. (auth/invalid-provider-id).'
87+
'Firebase: We have blocked all requests from this device due to unusual activity. Try again later. (auth/too-many-requests).'
8888
);
8989
});
9090
});

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

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,4 +786,119 @@ describe('core/auth/auth_impl', () => {
786786
expect(auth._getRecaptchaConfig()).to.eql(cachedRecaptchaConfigOFF);
787787
});
788788
});
789+
790+
context('passwordPolicy', () => {
791+
const TEST_ALLOWED_NON_ALPHANUMERIC_CHARS = ['!', '(', ')'];
792+
const TEST_MIN_PASSWORD_LENGTH = 6;
793+
794+
const passwordPolicyResponse = {
795+
customStrengthOptions: {
796+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH
797+
},
798+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
799+
schemaVersion: 1
800+
};
801+
const passwordPolicyResponseRequireNumeric = {
802+
customStrengthOptions: {
803+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
804+
containsNumericCharacter: true
805+
},
806+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
807+
schemaVersion: 1
808+
};
809+
const passwordPolicyResponseUnsupportedVersion = {
810+
customStrengthOptions: {
811+
maxPasswordLength: TEST_MIN_PASSWORD_LENGTH,
812+
unsupportedPasswordPolicyProperty: 10
813+
},
814+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
815+
schemaVersion: 0
816+
};
817+
const cachedPasswordPolicy = {
818+
customStrengthOptions: {
819+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH
820+
},
821+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
822+
};
823+
const cachedPasswordPolicyRequireNumeric = {
824+
customStrengthOptions: {
825+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
826+
containsNumericCharacter: true
827+
},
828+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
829+
};
830+
831+
beforeEach(async () => {
832+
mockFetch.setUp();
833+
mockEndpointWithParams(
834+
Endpoint.GET_PASSWORD_POLICY,
835+
{},
836+
passwordPolicyResponse
837+
);
838+
mockEndpointWithParams(
839+
Endpoint.GET_PASSWORD_POLICY,
840+
{
841+
tenantId: 'tenant-id'
842+
},
843+
passwordPolicyResponseRequireNumeric
844+
);
845+
mockEndpointWithParams(
846+
Endpoint.GET_PASSWORD_POLICY,
847+
{
848+
tenantId: 'tenant-id-with-unsupported-policy-version'
849+
},
850+
passwordPolicyResponseUnsupportedVersion
851+
);
852+
});
853+
854+
afterEach(() => {
855+
mockFetch.tearDown();
856+
});
857+
858+
it('password policy should be set for project if tenant ID is null', async () => {
859+
auth = await testAuth();
860+
auth.tenantId = null;
861+
await auth._updatePasswordPolicy();
862+
863+
expect(auth._getPasswordPolicy()).to.eql(cachedPasswordPolicy);
864+
});
865+
866+
it('password policy should be set for tenant if tenant ID is not null', async () => {
867+
auth = await testAuth();
868+
auth.tenantId = 'tenant-id';
869+
await auth._updatePasswordPolicy();
870+
871+
expect(auth._getPasswordPolicy()).to.eql(
872+
cachedPasswordPolicyRequireNumeric
873+
);
874+
});
875+
876+
it('password policy should dynamically switch if tenant ID switches.', async () => {
877+
auth = await testAuth();
878+
auth.tenantId = null;
879+
await auth._updatePasswordPolicy();
880+
881+
auth.tenantId = 'tenant-id';
882+
await auth._updatePasswordPolicy();
883+
884+
auth.tenantId = null;
885+
expect(auth._getPasswordPolicy()).to.eql(cachedPasswordPolicy);
886+
auth.tenantId = 'tenant-id';
887+
expect(auth._getPasswordPolicy()).to.eql(
888+
cachedPasswordPolicyRequireNumeric
889+
);
890+
auth.tenantId = 'other-tenant-id';
891+
expect(auth._getPasswordPolicy()).to.be.undefined;
892+
});
893+
894+
it('password policy should not be set when the schema version is not supported', async () => {
895+
auth = await testAuth();
896+
auth.tenantId = 'tenant-id-with-unsupported-policy-version';
897+
await expect(auth._updatePasswordPolicy()).to.be.rejectedWith(
898+
AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION
899+
);
900+
901+
expect(auth._getPasswordPolicy()).to.be.undefined;
902+
});
903+
});
789904
});

0 commit comments

Comments
 (0)