Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
security: enforce min length for user password (#1555)
* fix typing on auth context

* extract user password strength meter

* fix broken useToggle method

* extend form to accept arguments for validators

* enforce password length on update

* fix user password change form
  • Loading branch information
hay-kot committed Aug 14, 2022
1 parent b3c41a4 commit 54c4f19
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 95 deletions.
38 changes: 38 additions & 0 deletions frontend/components/Domain/User/UserPasswordStrength.vue
@@ -0,0 +1,38 @@
<template>
<div class="d-flex justify-center pb-6 mt-n1">
<div style="flex-basis: 500px">
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
<v-progress-linear
:value="pwStrength.score.value"
class="rounded-lg"
:color="pwStrength.color.value"
height="15"
/>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, toRef } from "@nuxtjs/composition-api";
import { usePasswordStrength } from "~/composables/use-passwords";
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
},
setup(props) {
const asRef = toRef(props, "value");
const pwStrength = usePasswordStrength(asRef);
return {
pwStrength,
};
},
});
</script>

<style scoped></style>
13 changes: 10 additions & 3 deletions frontend/components/global/AutoForm.vue
Expand Up @@ -187,9 +187,16 @@ export default defineComponent({
const list = [] as ((v: string) => boolean | string)[];
keys.forEach((key) => {
if (key in validators) {
// @ts-ignore TODO: fix this
list.push(validators[key]);
const split = key.split(":");
const validatorKey = split[0] as ValidatorKey;
if (validatorKey in validators) {
if (split.length === 1) {
// @ts-ignore- validators[validatorKey] is a function
list.push(validators[validatorKey]);
} else {
// @ts-ignore - validators[validatorKey] is a function
list.push(validators[validatorKey](split[1]));
}
}
});
return list;
Expand Down
9 changes: 6 additions & 3 deletions frontend/components/global/ToggleState.vue
Expand Up @@ -6,8 +6,7 @@
</template>

<script lang="ts">
import { defineComponent, watch } from "@nuxtjs/composition-api";
import { useToggle } from "@vueuse/core";
import { defineComponent, ref, watch } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
Expand All @@ -21,7 +20,11 @@ export default defineComponent({
},
},
setup(_, context) {
const [state, toggle] = useToggle();
const state = ref(false);
const toggle = () => {
state.value = !state.value;
};
watch(state, () => {
context.emit("input", state);
Expand Down
2 changes: 1 addition & 1 deletion frontend/composables/use-users/user-form.ts
Expand Up @@ -27,7 +27,7 @@ export const useUserForm = () => {
varName: "password",
disableUpdate: true,
type: fieldTypes.PASSWORD,
rules: ["required"],
rules: ["required", "minLength:8"],
},
{
section: "Permissions",
Expand Down
67 changes: 6 additions & 61 deletions frontend/pages/register/register.vue
Expand Up @@ -172,17 +172,9 @@
:rules="[validators.required, validators.minLength(8), validators.maxLength(258)]"
@click:append="pwFields.togglePasswordShow"
/>
<div class="d-flex justify-center pb-6 mt-n1">
<div style="flex-basis: 500px">
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
<v-progress-linear
:value="pwStrength.score.value"
class="rounded-lg"
:color="pwStrength.color.value"
height="15"
/>
</div>
</div>

<UserPasswordStrength :value="credentials.password1.value" />

<v-text-field
v-model="credentials.password2.value"
v-bind="inputAttrs"
Expand Down Expand Up @@ -272,9 +264,10 @@ import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { CreateUserRegistration } from "~/types/api-types/user";
import { VForm } from "~/types/vuetify";
import { usePasswordField, usePasswordStrength } from "~/composables/use-passwords";
import { usePasswordField } from "~/composables/use-passwords";
import { usePublicApi } from "~/composables/api/api-client";
import { useLocales } from "~/composables/use-locales";
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
const inputAttrs = {
filled: true,
Expand All @@ -284,59 +277,49 @@ const inputAttrs = {
};
export default defineComponent({
components: { UserPasswordStrength },
layout: "blank",
setup() {
const { i18n } = useContext();
const isDark = useDark();
function safeValidate(form: Ref<VForm | null>) {
if (form.value && form.value.validate) {
return form.value.validate();
}
return false;
}
// ================================================================
// Registration Context
//
// State is used to manage the registration process states and provide
// a state machine esq interface to interact with the registration workflow.
const state = useRegistration();
// ================================================================
// Handle Token URL / Initialization
//
const token = useRouteQuery("token");
// TODO: We need to have some way to check to see if the site is in a state
// Where it needs to be initialized with a user, in that case we'll handle that
// somewhere...
function initialUser() {
return false;
}
onMounted(() => {
if (token.value) {
state.setState(States.ProvideAccountDetails);
state.setType(RegistrationType.JoinGroup);
}
if (initialUser()) {
state.setState(States.ProvideGroupDetails);
state.setType(RegistrationType.InitialGroup);
}
});
// ================================================================
// Initial
const initial = {
createGroup: () => {
state.setState(States.ProvideGroupDetails);
state.setType(RegistrationType.CreateGroup);
if (token.value != null) {
token.value = null;
}
Expand All @@ -346,47 +329,36 @@ export default defineComponent({
state.setType(RegistrationType.JoinGroup);
},
};
// ================================================================
// Provide Token
const domTokenForm = ref<VForm | null>(null);
function validateToken() {
return true;
}
const provideToken = {
next: () => {
if (!safeValidate(domTokenForm as Ref<VForm>)) {
return;
}
if (validateToken()) {
state.setState(States.ProvideAccountDetails);
}
},
};
// ================================================================
// Provide Group Details
const publicApi = usePublicApi();
const domGroupForm = ref<VForm | null>(null);
const groupName = ref("");
const groupSeed = ref(false);
const groupPrivate = ref(false);
const groupErrorMessages = ref<string[]>([]);
const { validate: validGroupName, valid: groupNameValid } = useAsyncValidator(
groupName,
(v: string) => publicApi.validators.group(v),
i18n.tc("validation.group-name-is-taken"),
groupErrorMessages
);
const groupDetails = {
groupName,
groupSeed,
Expand All @@ -395,36 +367,29 @@ export default defineComponent({
if (!safeValidate(domGroupForm as Ref<VForm>) || !groupNameValid.value) {
return;
}
state.setState(States.ProvideAccountDetails);
},
};
// ================================================================
// Provide Account Details
const domAccountForm = ref<VForm | null>(null);
const username = ref("");
const email = ref("");
const advancedOptions = ref(false);
const usernameErrorMessages = ref<string[]>([]);
const { validate: validateUsername, valid: validUsername } = useAsyncValidator(
username,
(v: string) => publicApi.validators.username(v),
i18n.tc("validation.username-is-taken"),
usernameErrorMessages
);
const emailErrorMessages = ref<string[]>([]);
const { validate: validateEmail, valid: validEmail } = useAsyncValidator(
email,
(v: string) => publicApi.validators.email(v),
i18n.tc("validation.email-is-taken"),
emailErrorMessages
);
const accountDetails = {
username,
email,
Expand All @@ -433,37 +398,26 @@ export default defineComponent({
if (!safeValidate(domAccountForm as Ref<VForm>) || !validUsername.value || !validEmail.value) {
return;
}
state.setState(States.Confirmation);
},
};
// ================================================================
// Provide Credentials
const password1 = ref("");
const password2 = ref("");
const pwStrength = usePasswordStrength(password1);
const pwFields = usePasswordField();
const passwordMatch = () => password1.value === password2.value || i18n.tc("user.password-must-match");
const credentials = {
password1,
password2,
passwordMatch,
};
// ================================================================
// Locale
const { locale } = useLocales();
const langDialog = ref(false);
// ================================================================
// Confirmation
const confirmationData = computed(() => {
return [
{
Expand Down Expand Up @@ -498,10 +452,8 @@ export default defineComponent({
},
];
});
const api = useUserApi();
const router = useRouter();
async function submitRegistration() {
const payload: CreateUserRegistration = {
email: email.value,
Expand All @@ -511,23 +463,19 @@ export default defineComponent({
locale: locale.value,
seedData: groupSeed.value,
};
if (state.ctx.type === RegistrationType.CreateGroup) {
payload.group = groupName.value;
payload.advanced = advancedOptions.value;
payload.private = groupPrivate.value;
} else {
payload.groupToken = token.value;
}
const { response } = await api.register.register(payload);
if (response?.status === 201) {
alert.success("Registration Success");
router.push("/login");
}
}
return {
accountDetails,
confirmationData,
Expand All @@ -541,20 +489,17 @@ export default defineComponent({
langDialog,
provideToken,
pwFields,
pwStrength,
RegistrationType,
state,
States,
token,
usernameErrorMessages,
validators,
submitRegistration,
// Validators
validGroupName,
validateUsername,
validateEmail,
// Dom Refs
domAccountForm,
domGroupForm,
Expand Down

0 comments on commit 54c4f19

Please sign in to comment.