Skip to content

Commit ca662bd

Browse files
authored
Define implementation of internal password policy class (#7447)
* Define PasswordPolicyImpl * Add PasswordPolicyCustomStrengthOptions internal typing
1 parent e40b1e7 commit ca662bd

File tree

5 files changed

+236
-3
lines changed

5 files changed

+236
-3
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;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export interface GetPasswordPolicyResponse {
3737
customStrengthOptions: {
3838
minPasswordLength?: number;
3939
maxPasswordLength?: number;
40-
containsLowercaseLetter?: boolean;
41-
containsUppercaseLetter?: boolean;
40+
containsLowercaseCharacter?: boolean;
41+
containsUppercaseCharacter?: boolean;
4242
containsNumericCharacter?: boolean;
4343
containsNonAlphanumericCharacter?: boolean;
4444
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* @license
3+
* Copyright 2023 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 { PasswordPolicy } from '../../model/public_types';
22+
import { PasswordPolicyImpl } from './password_policy_impl';
23+
import { GetPasswordPolicyResponse } from '../../api/password_policy/get_password_policy';
24+
25+
use(sinonChai);
26+
use(chaiAsPromised);
27+
28+
describe('core/auth/password_policy_impl', () => {
29+
const TEST_MIN_PASSWORD_LENGTH = 6;
30+
const TEST_MAX_PASSWORD_LENGTH = 30;
31+
const TEST_CONTAINS_LOWERCASE = true;
32+
const TEST_CONTAINS_UPPERCASE = true;
33+
const TEST_CONTAINS_NUMERIC = true;
34+
const TEST_CONTAINS_NON_ALPHANUMERIC = true;
35+
const TEST_ALLOWED_NON_ALPHANUMERIC_CHARS = ['!', '(', ')'];
36+
const TEST_SCHEMA_VERSION = 1;
37+
const PASSWORD_POLICY_RESPONSE_REQUIRE_ALL: GetPasswordPolicyResponse = {
38+
customStrengthOptions: {
39+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
40+
maxPasswordLength: TEST_MAX_PASSWORD_LENGTH,
41+
containsLowercaseCharacter: TEST_CONTAINS_LOWERCASE,
42+
containsUppercaseCharacter: TEST_CONTAINS_UPPERCASE,
43+
containsNumericCharacter: TEST_CONTAINS_NUMERIC,
44+
containsNonAlphanumericCharacter: TEST_CONTAINS_NON_ALPHANUMERIC
45+
},
46+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
47+
schemaVersion: TEST_SCHEMA_VERSION
48+
};
49+
const PASSWORD_POLICY_RESPONSE_REQUIRE_LENGTH: GetPasswordPolicyResponse = {
50+
customStrengthOptions: {
51+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
52+
maxPasswordLength: TEST_MAX_PASSWORD_LENGTH
53+
},
54+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
55+
schemaVersion: TEST_SCHEMA_VERSION
56+
};
57+
const PASSWORD_POLICY_REQUIRE_ALL: PasswordPolicy = {
58+
customStrengthOptions: {
59+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
60+
maxPasswordLength: TEST_MAX_PASSWORD_LENGTH,
61+
containsLowercaseLetter: TEST_CONTAINS_LOWERCASE,
62+
containsUppercaseLetter: TEST_CONTAINS_UPPERCASE,
63+
containsNumericCharacter: TEST_CONTAINS_NUMERIC,
64+
containsNonAlphanumericCharacter: TEST_CONTAINS_UPPERCASE
65+
},
66+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
67+
};
68+
const PASSWORD_POLICY_REQUIRE_LENGTH: PasswordPolicy = {
69+
customStrengthOptions: {
70+
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
71+
maxPasswordLength: TEST_MAX_PASSWORD_LENGTH
72+
},
73+
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
74+
};
75+
76+
context('#PasswordPolicyImpl', () => {
77+
it('can construct the password policy from the backend response', () => {
78+
const policy: PasswordPolicy = new PasswordPolicyImpl(
79+
PASSWORD_POLICY_RESPONSE_REQUIRE_ALL
80+
);
81+
// The password policy contains the schema version internally, but the public typing does not.
82+
// Only check the fields that are publicly exposed.
83+
expect(policy.customStrengthOptions).to.eql(
84+
PASSWORD_POLICY_REQUIRE_ALL.customStrengthOptions
85+
);
86+
expect(policy.allowedNonAlphanumericCharacters).to.eql(
87+
PASSWORD_POLICY_REQUIRE_ALL.allowedNonAlphanumericCharacters
88+
);
89+
});
90+
91+
it('only includes requirements defined in the response', () => {
92+
const policy: PasswordPolicy = new PasswordPolicyImpl(
93+
PASSWORD_POLICY_RESPONSE_REQUIRE_LENGTH
94+
);
95+
expect(policy.customStrengthOptions).to.eql(
96+
PASSWORD_POLICY_REQUIRE_LENGTH.customStrengthOptions
97+
);
98+
expect(policy.allowedNonAlphanumericCharacters).to.eql(
99+
PASSWORD_POLICY_REQUIRE_LENGTH.allowedNonAlphanumericCharacters
100+
);
101+
// Requirements that are not in the response should be undefined.
102+
expect(policy.customStrengthOptions.containsLowercaseLetter).to.be
103+
.undefined;
104+
expect(policy.customStrengthOptions.containsUppercaseLetter).to.be
105+
.undefined;
106+
expect(policy.customStrengthOptions.containsNumericCharacter).to.be
107+
.undefined;
108+
expect(policy.customStrengthOptions.containsNonAlphanumericCharacter).to
109+
.be.undefined;
110+
});
111+
});
112+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright 2023 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 { GetPasswordPolicyResponse } from '../../api/password_policy/get_password_policy';
19+
import {
20+
PasswordPolicyCustomStrengthOptions,
21+
PasswordPolicyInternal,
22+
PasswordValidationStatusInternal
23+
} from '../../model/password_policy';
24+
import { PasswordValidationStatus } from '../../model/public_types';
25+
26+
/**
27+
* Stores password policy requirements and provides password validation against the policy.
28+
*
29+
* @internal
30+
*/
31+
export class PasswordPolicyImpl implements PasswordPolicyInternal {
32+
readonly customStrengthOptions: PasswordPolicyCustomStrengthOptions;
33+
readonly allowedNonAlphanumericCharacters: string[];
34+
readonly schemaVersion: number;
35+
36+
constructor(response: GetPasswordPolicyResponse) {
37+
// Only include custom strength options defined in the response.
38+
const responseOptions = response.customStrengthOptions;
39+
this.customStrengthOptions = {};
40+
if (responseOptions.minPasswordLength) {
41+
this.customStrengthOptions.minPasswordLength =
42+
responseOptions.minPasswordLength;
43+
}
44+
if (responseOptions.maxPasswordLength) {
45+
this.customStrengthOptions.maxPasswordLength =
46+
responseOptions.maxPasswordLength;
47+
}
48+
if (responseOptions.containsLowercaseCharacter !== undefined) {
49+
this.customStrengthOptions.containsLowercaseLetter =
50+
responseOptions.containsLowercaseCharacter;
51+
}
52+
if (responseOptions.containsUppercaseCharacter !== undefined) {
53+
this.customStrengthOptions.containsUppercaseLetter =
54+
responseOptions.containsUppercaseCharacter;
55+
}
56+
if (responseOptions.containsNumericCharacter !== undefined) {
57+
this.customStrengthOptions.containsNumericCharacter =
58+
responseOptions.containsNumericCharacter;
59+
}
60+
if (responseOptions.containsNonAlphanumericCharacter !== undefined) {
61+
this.customStrengthOptions.containsNonAlphanumericCharacter =
62+
responseOptions.containsNonAlphanumericCharacter;
63+
}
64+
65+
this.allowedNonAlphanumericCharacters =
66+
response.allowedNonAlphanumericCharacters;
67+
this.schemaVersion = response.schemaVersion;
68+
}
69+
70+
validatePassword(password: string): PasswordValidationStatus {
71+
const status: PasswordValidationStatusInternal = {
72+
isValid: false,
73+
passwordPolicy: this
74+
};
75+
76+
// TODO: Implement private helper methods for checking length and character options.
77+
// Call these here to populate the status object.
78+
if (password) {
79+
status.isValid = true;
80+
}
81+
82+
return status;
83+
}
84+
}

packages/auth/src/model/password_policy.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { PasswordPolicy, PasswordValidationStatus } from './public_types';
2525
* @internal
2626
*/
2727
export interface PasswordPolicyInternal extends PasswordPolicy {
28+
/**
29+
* Requirements enforced by the password policy.
30+
*/
31+
readonly customStrengthOptions: PasswordPolicyCustomStrengthOptions;
2832
/**
2933
* Schema version of the password policy.
3034
*/
@@ -36,6 +40,39 @@ export interface PasswordPolicyInternal extends PasswordPolicy {
3640
validatePassword(password: string): PasswordValidationStatus;
3741
}
3842

43+
/**
44+
* Internal typing of the password policy custom strength options that is modifiable. This
45+
* allows us to construct the strength options before storing them in the policy.
46+
*
47+
* @internal
48+
*/
49+
export interface PasswordPolicyCustomStrengthOptions {
50+
/**
51+
* Minimum password length.
52+
*/
53+
minPasswordLength?: number;
54+
/**
55+
* Maximum password length.
56+
*/
57+
maxPasswordLength?: number;
58+
/**
59+
* Whether the password should contain a lowercase letter.
60+
*/
61+
containsLowercaseLetter?: boolean;
62+
/**
63+
* Whether the password should contain an uppercase letter.
64+
*/
65+
containsUppercaseLetter?: boolean;
66+
/**
67+
* Whether the password should contain a numeric character.
68+
*/
69+
containsNumericCharacter?: boolean;
70+
/**
71+
* Whether the password should contain a non-alphanumeric character.
72+
*/
73+
containsNonAlphanumericCharacter?: boolean;
74+
}
75+
3976
/**
4077
* Internal typing of password validation status that is modifiable. This allows us to
4178
* construct the validation status before returning it.

0 commit comments

Comments
 (0)