From 4f3a150074f79a6a93c53bcf4bad765e169b7741 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 30 Apr 2024 16:58:40 -0400 Subject: [PATCH] Create a password strength UI for security dashboards plugin (#1762) * Initial password strength bar Signed-off-by: Derek Ho * save resolution Signed-off-by: Derek Ho * make the bar look nice Signed-off-by: Derek Ho * Updates according to UX Signed-off-by: Derek Ho * Add yarn.lock Signed-off-by: Derek Ho * Add step to install dependencies prior to building Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Add tests Signed-off-by: Derek Ho * Lint fix Signed-off-by: Derek Ho * Add it for the self reset panel and fix spacing Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Remove warning Signed-off-by: Derek Ho * Update the color and padding Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- package.json | 3 +- public/apps/account/password-reset-panel.tsx | 42 ++++++---- .../utils/password-edit-panel.tsx | 26 +++--- .../utils/password-strength-bar.tsx | 78 ++++++++++++++++++ .../password-strength-bar.test.tsx.snap | 31 ++++++++ .../utils/test/password-strength-bar.test.tsx | 79 +++++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 public/apps/configuration/utils/password-strength-bar.tsx create mode 100644 public/apps/configuration/utils/test/__snapshots__/password-strength-bar.test.tsx.snap create mode 100644 public/apps/configuration/utils/test/password-strength-bar.test.tsx diff --git a/package.json b/package.json index a886c2939..944c3f426 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/apps/account/password-reset-panel.tsx b/public/apps/account/password-reset-panel.tsx index 97d45400e..703d617e9 100644 --- a/public/apps/account/password-reset-panel.tsx +++ b/public/apps/account/password-reset-panel.tsx @@ -19,6 +19,9 @@ import { EuiButtonEmpty, EuiCallOut, EuiFieldPassword, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, EuiModal, EuiModalBody, EuiModalFooter, @@ -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; @@ -121,22 +125,30 @@ export function PasswordResetPanel(props: PasswordResetPanelProps) { isInvalid={isCurrentPasswordInvalid} /> + - - ) { - setNewPassword(e.target.value); - setIsNewPasswordInvalid(false); - setIsRepeatNewPasswordInvalid(repeatNewPassword !== newPassword); - }} - isInvalid={isNewPasswordInvalid} - /> - + + + + ) { + setNewPassword(e.target.value); + setIsNewPasswordInvalid(false); + setIsRepeatNewPasswordInvalid(repeatNewPassword !== newPassword); + }} + isInvalid={isNewPasswordInvalid} + /> + + + + + + - - } - type="password" - onChange={passwordChangeHandler} - /> - + + + + } + type="password" + onChange={passwordChangeHandler} + /> + + + + + + { + 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 && ( + + + + + {passwordStrength.feedback.suggestions && ( + + + {passwordStrength.feedback.suggestions} + + + )} + + + ) + ); +}; diff --git a/public/apps/configuration/utils/test/__snapshots__/password-strength-bar.test.tsx.snap b/public/apps/configuration/utils/test/__snapshots__/password-strength-bar.test.tsx.snap new file mode 100644 index 000000000..6644cc5fe --- /dev/null +++ b/public/apps/configuration/utils/test/__snapshots__/password-strength-bar.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Password strength bar tests renders 1`] = ` + + + + + + + Add another word or two. Uncommon words are better. + + + + +`; diff --git a/public/apps/configuration/utils/test/password-strength-bar.test.tsx b/public/apps/configuration/utils/test/password-strength-bar.test.tsx new file mode 100644 index 000000000..30fd37b9f --- /dev/null +++ b/public/apps/configuration/utils/test/password-strength-bar.test.tsx @@ -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(); + expect(component).toMatchSnapshot(); + }); + + it('verifies feedback, warning for very weak password', () => { + const component = render(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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 + }); +}); diff --git a/yarn.lock b/yarn.lock index 5650f081a..56cd2a0fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==