Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a Passwort Strength Component for User Registration #19355

Draft
wants to merge 10 commits into
base: dev
Choose a base branch
from
3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"@types/jest": "^29.5.12",
"@vueuse/core": "^10.5.0",
"@vueuse/math": "^10.9.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
"assert": "^2.1.0",
"axios": "^1.6.2",
"babel-runtime": "^6.26.0",
Expand Down
172 changes: 172 additions & 0 deletions client/src/components/Login/PasswordStrength.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<script setup lang="ts">
import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";
import * as zxcvbnCommonPackage from "@zxcvbn-ts/language-common";
import * as zxcvbnEnPackage from "@zxcvbn-ts/language-en";
import { type PropType, type Ref, ref, watch } from "vue";

const props = defineProps({
password: {
type: String as PropType<string | null>,
required: true,
},
});

const passwordStrength = ref<string>("empty");
const strengthScore = ref<number>(0);
const showPasswordGuidelines: Ref<boolean> = ref(false);
const crackTime = ref<string>("");
const options = {
translations: zxcvbnEnPackage.translations,
graphs: zxcvbnCommonPackage.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary,
},
};
zxcvbnOptions.setOptions(options);

function evaluatePasswordStrength(newPassword: string) {
if (newPassword.length === 0) {
passwordStrength.value = "empty";
strengthScore.value = 0;
crackTime.value = "";
return;
}

const result = zxcvbn(newPassword);
strengthScore.value = result.score;
crackTime.value = String(result.crackTimesDisplay.onlineNoThrottling10PerSecond || "N/A");

if (strengthScore.value <= 1) {
passwordStrength.value = "weak";
} else if (strengthScore.value <= 3) {
passwordStrength.value = "medium";
} else {
passwordStrength.value = "strong";
}
}

watch(
() => props.password,
(newPassword) => {
if (typeof newPassword === "string") {
evaluatePasswordStrength(newPassword);
}
}
);
</script>

<template>
<div>
<div class="password-strength-bar-container mt-2">
<div
class="password-strength-bar"
:class="passwordStrength"
:style="{ width: `${(strengthScore / 4) * 100}%` }"></div>
</div>

<div :class="['password-strength', passwordStrength]" class="mt-2">
<span v-if="passwordStrength === 'empty'"></span>
<span v-else-if="passwordStrength === 'weak'">Weak Password</span>
<span v-else-if="passwordStrength === 'medium'">Medium Password</span>
<span v-else>Strong Password</span>
</div>

<div v-if="passwordStrength !== 'empty'" class="crack-time mt-2">
<strong>Estimated time to crack:</strong>
<span :class="passwordStrength"> {{ crackTime }}</span>
</div>

<BButton variant="secondary" class="ui-link mt-3" @click="showPasswordGuidelines = true">
Password Guidelines
</BButton>

<div>
<BModal v-model="showPasswordGuidelines" title="Tips for a secure Password">
<p>A good password should meet the following criteria:</p>
<ul>
<li>At least 11 characters long.</li>
<li>Use uppercase and lowercase letters.</li>
<li>At least one number and one special character.</li>
<li>Avoid common passwords like <code>123456</code> or <code>password</code>.</li>
<li>No repeated patterns like <code>aaaa</code> or <code>123123</code>.</li>
</ul>
<p>
Learn more about:
<a
href="https://www.cisa.gov/secure-our-world/use-strong-passwords target="
target="_blank"
rel="noopener noreferrer"
>strong passwords</a
>.
</p>
<template v-slot:modal-footer>
<BButton variant="secondary" @click="showPasswordGuidelines = false">Schließen</BButton>
</template>
</BModal>
</div>
</div>
</template>

<style scoped lang="scss">
@import "theme/blue.scss";

.password-strength-bar-container {
background-color: $gray-200;
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 5px;
}

.password-strength-bar {
height: 100%;
transition: width 0.3s ease;

&.weak {
background-color: $brand-danger;
}

&.medium {
background-color: $brand-warning;
}

&.strong {
background-color: $brand-success;
}
}

.password-strength,
.crack-time span {
&.weak {
color: $brand-danger;
}

&.medium {
color: $brand-warning;
}

&.strong {
color: $brand-success;
}
}

.password-help ul {
list-style: none;
padding: 0;
}

.password-help li {
display: flex;
align-items: center;
margin: 0.2rem;
}

.password-help li.rule-met {
color: $brand-success;
}

.password-help li .fa {
margin-right: 0.5rem;
}
</style>
69 changes: 61 additions & 8 deletions client/src/components/Login/RegisterForm.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import {
BAlert,
Expand All @@ -21,6 +23,7 @@ import localize from "@/utils/localization";
import { withPrefix } from "@/utils/redirect";
import { errorMessageAsString } from "@/utils/simple-error";

import PasswordStrength from "@/components/Login/PasswordStrength.vue";
import ExternalLogin from "@/components/User/ExternalIdentities/ExternalLogin.vue";

interface Props {
Expand All @@ -36,14 +39,15 @@ interface Props {
}

const props = defineProps<Props>();
const showPassword = ref(false);

const emit = defineEmits<{
(e: "toggle-login"): void;
}>();

const email = ref(null);
const confirm = ref(null);
const password = ref(null);
const password = ref<string | null>(null);
const username = ref(null);
const subscribe = ref(null);
const messageText: Ref<string | null> = ref(null);
Expand All @@ -62,6 +66,10 @@ function toggleLogin() {
emit("toggle-login");
}

function togglePasswordVisibility() {
showPassword.value = !showPassword.value;
}

async function submit() {
disableCreate.value = true;

Expand Down Expand Up @@ -139,13 +147,22 @@ async function submit() {
</BFormGroup>

<BFormGroup :label="labelPassword" label-for="register-form-password">
<BFormInput
id="register-form-password"
v-model="password"
name="password"
type="password"
autocomplete="new-password"
required />
<div class="input-group">
<BFormInput
id="register-form-password"
v-model="password"
name="password"
:type="showPassword ? 'text' : 'password'"
autocomplete="new-password"
required />
<button
type="button"
class="input-group-text password-toggle-icon"
@click.prevent="togglePasswordVisibility">
<FontAwesomeIcon :icon="showPassword ? faEyeSlash : faEye" />
</button>
</div>
<PasswordStrength :password="password" />
</BFormGroup>

<BFormGroup :label="labelConfirmPassword" label-for="register-form-confirm">
Expand Down Expand Up @@ -217,7 +234,10 @@ async function submit() {
</div>
</div>
</template>

<style scoped lang="scss">
@import "theme/blue.scss";

.embed-container {
position: relative;

Expand All @@ -239,4 +259,37 @@ async function submit() {
border-radius: 4px;
}
}

.input-group {
background-color: transparent;
position: relative;
display: flex;
align-items: center;

input {
flex: 1;
padding-right: 2.5rem;
}

.password-toggle-icon {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
font-size: 1rem;
color: $gray-600;

&:hover {
color: $gray-900;
}
}

.input-group-text {
background: transparent;
border: none;
padding: 0;
z-index: 10;
}
}
</style>
21 changes: 19 additions & 2 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3137,6 +3137,23 @@
resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==

"@zxcvbn-ts/core@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/core/-/core-3.0.4.tgz#c5bde72235eb6c273cec78b672bb47c0d7045cad"
integrity sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==
dependencies:
fastest-levenshtein "1.0.16"

"@zxcvbn-ts/language-common@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-common/-/language-common-3.0.4.tgz#fa1d2a42f8c8a589555859795da90d6b8027b7c4"
integrity sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==

"@zxcvbn-ts/language-en@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz#162ada6b2b556444efd5a7700e70845cfde6d6ec"
integrity sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==

abab@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
Expand Down Expand Up @@ -6152,9 +6169,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==

fastest-levenshtein@^1.0.12:
fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.12:
version "1.0.16"
resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==

fastq@^1.6.0:
Expand Down
Loading