Skip to content

Commit

Permalink
Create a password strength UI for security dashboards plugin (opensea…
Browse files Browse the repository at this point in the history
…rch-project#1762)

* Initial password strength bar

Signed-off-by: Derek Ho <[email protected]>

* save resolution

Signed-off-by: Derek Ho <[email protected]>

* make the bar look nice

Signed-off-by: Derek Ho <[email protected]>

* Updates according to UX

Signed-off-by: Derek Ho <[email protected]>

* Add yarn.lock

Signed-off-by: Derek Ho <[email protected]>

* Add step to install dependencies prior to building

Signed-off-by: Derek Ho <[email protected]>

* Lint

Signed-off-by: Derek Ho <[email protected]>

* Add tests

Signed-off-by: Derek Ho <[email protected]>

* Lint fix

Signed-off-by: Derek Ho <[email protected]>

* Add it for the self reset panel and fix spacing

Signed-off-by: Derek Ho <[email protected]>

* Lint

Signed-off-by: Derek Ho <[email protected]>

* Remove warning

Signed-off-by: Derek Ho <[email protected]>

* Update the color and padding

Signed-off-by: Derek Ho <[email protected]>

---------

Signed-off-by: Derek Ho <[email protected]>
  • Loading branch information
derek-ho authored Apr 30, 2024
1 parent 23604d1 commit 4f3a150
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 25 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"dependencies": {
"@hapi/cryptiles": "5.0.0",
"@hapi/wreck": "^17.1.0",
"html-entities": "1.3.1"
"html-entities": "1.3.1",
"zxcvbn": "^4.4.2"
},
"resolutions": {
"selenium-webdriver": "4.10.0",
Expand Down
42 changes: 27 additions & 15 deletions public/apps/account/password-reset-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
EuiButtonEmpty,
EuiCallOut,
EuiFieldPassword,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
Expand All @@ -33,6 +36,7 @@ import { PASSWORD_INSTRUCTION } from '../apps-constants';
import { constructErrorMessageAndLog } from '../error-utils';
import { validateCurrentPassword } from '../../utils/login-utils';
import { getDashboardsInfo } from '../../utils/dashboards-info-utils';
import { PasswordStrengthBar } from '../configuration/utils/password-strength-bar';

interface PasswordResetPanelProps {
coreStart: CoreStart;
Expand Down Expand Up @@ -121,22 +125,30 @@ export function PasswordResetPanel(props: PasswordResetPanelProps) {
isInvalid={isCurrentPasswordInvalid}
/>
</FormRow>
<EuiSpacer />

<FormRow
headerText="New password"
helpText={passwordHelpText}
isInvalid={isNewPasswordInvalid}
>
<EuiFieldPassword
data-test-subj="new-password"
onChange={function (e: React.ChangeEvent<HTMLInputElement>) {
setNewPassword(e.target.value);
setIsNewPasswordInvalid(false);
setIsRepeatNewPasswordInvalid(repeatNewPassword !== newPassword);
}}
isInvalid={isNewPasswordInvalid}
/>
</FormRow>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<FormRow
headerText="New password"
helpText={passwordHelpText}
isInvalid={isNewPasswordInvalid}
>
<EuiFieldPassword
data-test-subj="new-password"
onChange={function (e: React.ChangeEvent<HTMLInputElement>) {
setNewPassword(e.target.value);
setIsNewPasswordInvalid(false);
setIsRepeatNewPasswordInvalid(repeatNewPassword !== newPassword);
}}
isInvalid={isNewPasswordInvalid}
/>
</FormRow>
<EuiFormRow>
<PasswordStrengthBar password={newPassword} />
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

<FormRow
headerText="Re-enter new password"
Expand Down
26 changes: 17 additions & 9 deletions public/apps/configuration/utils/password-edit-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@

import React from 'react';
import { CoreStart } from 'opensearch-dashboards/public';
import { EuiFieldText, EuiIcon } from '@elastic/eui';
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui';
import { FormRow } from './form-row';
import { PASSWORD_INSTRUCTION } from '../../apps-constants';
import { getDashboardsInfo } from '../../../utils/dashboards-info-utils';
import { PasswordStrengthBar } from './password-strength-bar';

export function PasswordEditPanel(props: {
coreStart: CoreStart;
Expand Down Expand Up @@ -61,14 +62,21 @@ export function PasswordEditPanel(props: {

return (
<>
<FormRow headerText="Password" helpText={passwordHelpText}>
<EuiFieldText
data-test-subj="password"
prepend={<EuiIcon type="lock" />}
type="password"
onChange={passwordChangeHandler}
/>
</FormRow>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<FormRow headerText="Password" helpText={passwordHelpText}>
<EuiFieldText
data-test-subj="password"
prepend={<EuiIcon type="lock" />}
type="password"
onChange={passwordChangeHandler}
/>
</FormRow>
<EuiFormRow>
<PasswordStrengthBar password={password} />
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

<FormRow
headerText="Re-enter password"
Expand Down
78 changes: 78 additions & 0 deletions public/apps/configuration/utils/password-strength-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright OpenSearch Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
import React from 'react';
import zxcvbn from 'zxcvbn';

interface PasswordStrengthBarProps {
password: string;
}

export const PasswordStrengthBar = (props: PasswordStrengthBarProps) => {
const { password } = props;
const passwordStrength = zxcvbn(password);
const strength = passwordStrength.score;
let message;
let color;
switch (strength) {
case 0:
message = 'Very weak';
color = 'danger';
break;
case 1:
message = 'Weak';
color = 'danger';
break;
case 2:
message = 'Ok';
color = 'warning';
break;
case 3:
message = 'Strong';
color = 'success';
break;
case 4:
message = 'Very strong';
color = 'success';
break;
}

return (
password && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiProgress
value={strength}
max={4}
size="m"
color={color}
valueText={message}
label={'Password strength'}
data-test-subj="password-strength-progress"
/>
</EuiFlexItem>
{passwordStrength.feedback.suggestions && (
<EuiFlexItem>
<EuiText size="xs" data-test-subj="password-strength-feedback-suggestions">
{passwordStrength.feedback.suggestions}
</EuiText>
</EuiFlexItem>
)}
<EuiSpacer size="s" />
</EuiFlexGroup>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Password strength bar tests renders 1`] = `
<EuiFlexGroup
direction="column"
gutterSize="xs"
>
<EuiFlexItem>
<EuiProgress
color="danger"
data-test-subj="password-strength-progress"
label="Password strength"
max={4}
size="m"
value={0}
valueText="Very weak"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText
data-test-subj="password-strength-feedback-suggestions"
size="xs"
>
Add another word or two. Uncommon words are better.
</EuiText>
</EuiFlexItem>
<EuiSpacer
size="s"
/>
</EuiFlexGroup>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright OpenSearch Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import { render, shallow } from 'enzyme';
import React from 'react';
import { PasswordStrengthBar } from '../password-strength-bar';

describe('Password strength bar tests', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('renders', () => {
const component = shallow(<PasswordStrengthBar password="test" />);
expect(component).toMatchSnapshot();
});

it('verifies feedback, warning for very weak password', () => {
const component = render(<PasswordStrengthBar password="test" />);

const suggestions = component.find('[data-test-subj="password-strength-feedback-suggestions"]');
expect(suggestions.text()).toBe('Add another word or two. Uncommon words are better.');

const score = component.find('[data-test-subj="password-strength-progress"]');
expect(score.prop('value')).toBe('0'); // test is considered very weak
});

it('verifies feedback, warning for weak password', () => {
const component = render(<PasswordStrengthBar password="test12" />);

const suggestions = component.find('[data-test-subj="password-strength-feedback-suggestions"]');
expect(suggestions.text()).toBe('Add another word or two. Uncommon words are better.');

const score = component.find('[data-test-subj="password-strength-progress"]');
expect(score.prop('value')).toBe('1'); // test12 is considered weak
});

it('verifies feedback, warning for an ok password', () => {
const component = render(<PasswordStrengthBar password="test124My" />);

const suggestions = component.find('[data-test-subj="password-strength-feedback-suggestions"]');
expect(suggestions.text()).toBe('Add another word or two. Uncommon words are better.');

const score = component.find('[data-test-subj="password-strength-progress"]');
expect(score.prop('value')).toBe('2'); // test124My is considered ok
});

it('verifies feedback, warning for a strong password', () => {
const component = render(<PasswordStrengthBar password="test124MyTable" />);

const suggestions = component.find('[data-test-subj="password-strength-feedback-suggestions"]');
expect(suggestions.text()).toBe('');

const score = component.find('[data-test-subj="password-strength-progress"]');
expect(score.prop('value')).toBe('3'); // test124MyTable is considered strong
});

it('verifies feedback, warning for very strong password', () => {
const component = render(<PasswordStrengthBar password="myStrongPassword123!" />);

const suggestions = component.find('[data-test-subj="password-strength-feedback-suggestions"]');
expect(suggestions.text()).toBe('');

const score = component.find('[data-test-subj="password-strength-progress"]');
expect(score.prop('value')).toBe('4'); // myStrongPassword123! is considered very strong
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5161,3 +5161,8 @@ yauzl@^2.10.0:
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"

zxcvbn@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==

0 comments on commit 4f3a150

Please sign in to comment.