Skip to content

Commit

Permalink
Password validation with zxcvbn (#42)
Browse files Browse the repository at this point in the history
* add password validation via zxcvbn

* lazy load options

* fix imports

* clean up if statement

* prettier
  • Loading branch information
cmintey authored Apr 17, 2023
1 parent 5e6660d commit 39faa82
Show file tree
Hide file tree
Showing 17 changed files with 166 additions and 65 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
"@lucia-auth/adapter-prisma": "^1.0.0",
"@prisma/client": "^4.12.0",
"@samirrayani/metascraper-shopping": "^1.4.22",
"@zxcvbn-ts/core": "^2.2.1",
"@zxcvbn-ts/language-common": "^2.0.1",
"@zxcvbn-ts/language-en": "^2.1.0",
"got-scraping": "^3.2.13",
"handlebars": "^4.7.7",
"lucia-auth": "^1.0.0",
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions src/lib/components/PasswordInput.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
<script lang="ts">
import { zxcvbn, zxcvbnOptions, type ZxcvbnResult } from "@zxcvbn-ts/core";
import { loadOptions } from "$lib/zxcvbn";
import { popup, ProgressBar, type PopupSettings } from "@skeletonlabs/skeleton";
import { onMount } from "svelte";
export let id: string;
export let name: string | undefined = undefined;
export let label: string;
export let required = false;
export let autocomplete: string | null | undefined = undefined;
export let error = false;
export let value: string | null | undefined = "";
export let strengthMeter = false;
onMount(async () => {
if (strengthMeter) {
const options = await loadOptions();
zxcvbnOptions.setOptions(options);
}
});
let strength: ZxcvbnResult | undefined;
$: strength = value ? zxcvbn(value) : undefined;
let visible = false;
const handleClick = () => {
visible = !visible;
};
const meterLookup = [
"bg-error-500",
"bg-error-500",
"bg-error-500",
"bg-warning-500",
"bg-success-500"
];
const popupSettings: PopupSettings = {
event: "hover",
target: "suggestions",
placement: "right"
};
</script>

<label for={id}>
Expand All @@ -34,8 +64,42 @@
id="showpassword"
on:click|preventDefault={handleClick}
on:keypress|preventDefault
tabindex="-1"
>
<iconify-icon icon="ion:{visible ? 'eye-off' : 'eye'}" class="-mb-0.5" />
</button>
</div>
</label>

{#if strengthMeter && value !== "" && strength}
<div class="flex flex-row items-center space-x-1">
<ProgressBar
label="Password Strength"
value={strength.score + 1}
max={5}
bind:meter={meterLookup[strength.score.valueOf()]}
/>
<div
class="flex items-center"
class:hidden={strength.feedback.suggestions.length === 0 && !strength.feedback.warning}
use:popup={popupSettings}
>
<iconify-icon icon="ion:information-circle-outline" />
</div>
</div>

<div class="card variant-filled p-4" data-popup="suggestions">
{#each strength.feedback.suggestions as suggestion}
<p>{suggestion}</p>
{/each}
<p>{strength.feedback.warning}</p>
</div>

{#if strength.score < 3}
<p>Weak</p>
{:else if strength.score < 4}
<p>Moderate</p>
{:else}
<p>Strong</p>
{/if}
{/if}
16 changes: 10 additions & 6 deletions src/lib/components/account/ChangePassword.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@
autocomplete="current-password"
bind:value={passwordReset.current}
/>
<PasswordInput
label="New Password"
id="newpassword"
autocomplete="new-password"
bind:value={passwordReset.new}
/>
<div>
<PasswordInput
label="New Password"
id="newpassword"
autocomplete="new-password"
bind:value={passwordReset.new}
strengthMeter
/>
</div>

<PasswordInput
label="Confirm Password"
id="confirmpassword"
Expand Down
40 changes: 40 additions & 0 deletions src/lib/validations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";
import { loadOptions } from "$lib/zxcvbn";

await loadOptions().then((options) => zxcvbnOptions.setOptions(options));

const passwordZxcvbn = z.string().refine((pass) => zxcvbn(pass).score > 2, {
message: "Password must be at least 'moderate'"
});

export const signupSchema = z.object({
name: z.string().trim().min(1, "Name must not be blank"),
username: z.string().trim().min(1, "Username must not be blank"),
email: z.string().email(),
password: passwordZxcvbn,
tokenId: z.string().optional()
});

export const loginSchema = z.object({
username: z.string().trim().min(1, "Username must not be blank"),
password: z.string().min(1, "Password must not be blank")
});

export const resetPasswordSchema = z.object({
oldPassword: z.string().min(1),
newPassword: passwordZxcvbn
});

export const settingSchema = z.object({
enableSignup: z.coerce.boolean().default(false),
enableSuggestions: z.coerce.boolean().default(false),
suggestionMethod: z.enum(["surprise", "auto-approval", "approval"]).default("approval"),
enableSMTP: z.coerce.boolean().default(false),
smtpHost: z.string().optional(),
smtpPort: z.coerce.number().optional(),
smtpUser: z.string().optional(),
smtpPass: z.string().optional(),
smtpFrom: z.string().optional(),
smtpFromName: z.string().optional()
});
6 changes: 0 additions & 6 deletions src/lib/validations/login.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/lib/validations/resetPassword.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/lib/validations/settings.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/lib/validations/signup.ts

This file was deleted.

13 changes: 13 additions & 0 deletions src/lib/zxcvbn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const loadOptions = async () => {
const zxcvbnCommonPackage = await import("@zxcvbn-ts/language-common");
const zxcvbnEnPackage = await import("@zxcvbn-ts/language-en");

return {
dictionary: {
...zxcvbnCommonPackage.default.dictionary,
...zxcvbnEnPackage.default.dictionary
},
graphs: zxcvbnCommonPackage.default.adjacencyGraphs,
translations: zxcvbnEnPackage.default.translations
};
};
2 changes: 1 addition & 1 deletion src/routes/account/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from "$lib/server/auth";
import { client } from "$lib/server/prisma";
import { resetPasswordSchema } from "$lib/validations/resetPassword";
import { resetPasswordSchema } from "$lib/validations";
import type { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { fail, redirect } from "@sveltejs/kit";
import { writeFileSync } from "fs";
Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin/groups/[groupId]/settings/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getConfig, writeConfig } from "$lib/server/config";
import { client } from "$lib/server/prisma";
import { redirect, error, fail, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { settingSchema } from "$lib/validations/settings";
import { settingSchema } from "$lib/validations";

export const load = (async ({ locals, params }) => {
const { session, user } = await locals.validateUser();
Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin/settings/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getConfig, writeConfig } from "$lib/server/config";
import { redirect, error, fail, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { sendTest } from "$lib/server/email";
import { settingSchema } from "$lib/validations/settings";
import { settingSchema } from "$lib/validations";
import type { z } from "zod";

export const load: PageServerLoad = async ({ locals }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fail, redirect } from "@sveltejs/kit";
import { auth } from "$lib/server/auth";
import type { PageServerLoad, Actions } from "./$types";
import { loginSchema } from "$lib/validations/login";
import { loginSchema } from "$lib/validations";
import { getConfig } from "$lib/server/config";

// If the user exists, redirect authenticated users to the profile page.
Expand Down
2 changes: 1 addition & 1 deletion src/routes/reset-password/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from "$lib/server/auth";
import { client } from "$lib/server/prisma";
import { hashToken } from "$lib/server/token";
import { resetPasswordSchema } from "$lib/validations/resetPassword";
import { resetPasswordSchema } from "$lib/validations";
import { error, fail } from "@sveltejs/kit";
import { z } from "zod";
import type { Actions, PageServerLoad } from "./$types";
Expand Down
2 changes: 1 addition & 1 deletion src/routes/signup/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from "$lib/server/auth";
import { client } from "$lib/server/prisma";
import { signupSchema } from "$lib/validations/signup";
import { signupSchema } from "$lib/validations";
import { error, fail, redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { hashToken } from "$lib/server/token";
Expand Down
2 changes: 2 additions & 0 deletions src/routes/signup/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { enhance } from "$app/forms";
import PasswordInput from "$lib/components/PasswordInput.svelte";
import { onMount } from "svelte";
import type { ActionData, PageData } from "./$types";
export let data: PageData;
Expand Down Expand Up @@ -53,6 +54,7 @@
name="password"
required
bind:value={password}
strengthMeter
/>
<PasswordInput
label="Confirm Password"
Expand Down

0 comments on commit 39faa82

Please sign in to comment.