Skip to content

Commit c54b3fd

Browse files
committed
prevent use of common password using the haveibeenpwned password api
1 parent 468c5e5 commit c54b3fd

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

app/routes/_auth+/onboarding.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import crypto from 'node:crypto'
12
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
23
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
34
import { data, redirect, Form, useSearchParams } from 'react-router'
@@ -72,6 +73,27 @@ export async function action({ request }: Route.ActionArgs) {
7273
})
7374
return
7475
}
76+
const hash = crypto
77+
.createHash('sha1')
78+
.update(data.password, 'utf8')
79+
.digest('hex')
80+
.toUpperCase()
81+
const [prefix, suffix] = [hash.slice(0, 5), hash.slice(5)]
82+
const res = await fetch(
83+
`https://api.pwnedpasswords.com/range/${prefix}`,
84+
)
85+
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
86+
const text = await res.text()
87+
const matches = text
88+
.split('/\r?\n/')
89+
.filter((line) => line.includes(suffix))
90+
if (matches.length) {
91+
ctx.addIssue({
92+
path: ['password'],
93+
code: 'custom',
94+
message: 'Password is too common',
95+
})
96+
}
7597
}).transform(async (data) => {
7698
if (intent !== null) return { ...data, session: null }
7799

app/routes/_auth+/reset-password.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import crypto from 'node:crypto'
12
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
23
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
34
import { type SEOHandle } from '@nasa-gcn/remix-seo'
@@ -41,8 +42,29 @@ export async function loader({ request }: Route.LoaderArgs) {
4142
export async function action({ request }: Route.ActionArgs) {
4243
const resetPasswordUsername = await requireResetPasswordUsername(request)
4344
const formData = await request.formData()
44-
const submission = parseWithZod(formData, {
45-
schema: ResetPasswordSchema,
45+
const submission = await parseWithZod(formData, {
46+
schema: ResetPasswordSchema.superRefine(async ({ password }, ctx) => {
47+
const hash = crypto
48+
.createHash('sha1')
49+
.update(password, 'utf8')
50+
.digest('hex')
51+
.toUpperCase()
52+
const [prefix, suffix] = [hash.slice(0, 5), hash.slice(5)]
53+
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`)
54+
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
55+
const data = await res.text()
56+
const matches = data
57+
.split('/\r?\n/')
58+
.filter((line) => line.includes(suffix))
59+
if (matches.length) {
60+
ctx.addIssue({
61+
path: ['password'],
62+
code: 'custom',
63+
message: 'Password is too common',
64+
})
65+
}
66+
}),
67+
async: true,
4668
})
4769
if (submission.status !== 'success') {
4870
return data(

0 commit comments

Comments
 (0)