Skip to content

Commit 963b990

Browse files
committed
made requested chages
1. Reduced the core logic by extracting it in custom function in auth.server.ts 2. added timeout to the request to skip the check if request takes more than 1s 3. skips the check if request fails 4. added msw mock so that we don't depend upon it in development and testing
1 parent c54b3fd commit 963b990

File tree

5 files changed

+56
-32
lines changed

5 files changed

+56
-32
lines changed

app/routes/_auth+/onboarding.tsx

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import crypto from 'node:crypto'
21
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
32
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
43
import { data, redirect, Form, useSearchParams } from 'react-router'
@@ -8,7 +7,12 @@ import { z } from 'zod'
87
import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
98
import { Spacer } from '#app/components/spacer.tsx'
109
import { StatusButton } from '#app/components/ui/status-button.tsx'
11-
import { requireAnonymous, sessionKey, signup } from '#app/utils/auth.server.ts'
10+
import {
11+
checkCommonPassword,
12+
requireAnonymous,
13+
sessionKey,
14+
signup,
15+
} from '#app/utils/auth.server.ts'
1216
import { prisma } from '#app/utils/db.server.ts'
1317
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
1418
import { useIsPending } from '#app/utils/misc.tsx'
@@ -73,21 +77,8 @@ export async function action({ request }: Route.ActionArgs) {
7377
})
7478
return
7579
}
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) {
80+
const isCommonPassword = await checkCommonPassword(data.password)
81+
if (isCommonPassword) {
9182
ctx.addIssue({
9283
path: ['password'],
9384
code: 'custom',

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

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import crypto from 'node:crypto'
21
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
32
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
43
import { type SEOHandle } from '@nasa-gcn/remix-seo'
54
import { data, redirect, Form } from 'react-router'
65
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
76
import { ErrorList, Field } from '#app/components/forms.tsx'
87
import { StatusButton } from '#app/components/ui/status-button.tsx'
9-
import { requireAnonymous, resetUserPassword } from '#app/utils/auth.server.ts'
8+
import {
9+
checkCommonPassword,
10+
requireAnonymous,
11+
resetUserPassword,
12+
} from '#app/utils/auth.server.ts'
1013
import { useIsPending } from '#app/utils/misc.tsx'
1114
import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts'
1215
import { verifySessionStorage } from '#app/utils/verification.server.ts'
@@ -44,19 +47,8 @@ export async function action({ request }: Route.ActionArgs) {
4447
const formData = await request.formData()
4548
const submission = await parseWithZod(formData, {
4649
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) {
50+
const isCommonPassword = await checkCommonPassword(password)
51+
if (isCommonPassword) {
6052
ctx.addIssue({
6153
path: ['password'],
6254
code: 'custom',

app/utils/auth.server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import crypto from 'node:crypto'
12
import { type Connection, type Password, type User } from '@prisma/client'
23
import bcrypt from 'bcryptjs'
34
import { redirect } from 'react-router'
@@ -255,3 +256,33 @@ export async function verifyUserPassword(
255256

256257
return { id: userWithPassword.id }
257258
}
259+
260+
export async function checkCommonPassword(password: string) {
261+
const hash = crypto
262+
.createHash('sha1')
263+
.update(password, 'utf8')
264+
.digest('hex')
265+
.toUpperCase()
266+
267+
const [prefix, suffix] = [hash.slice(0, 5), hash.slice(5)]
268+
269+
const controller = new AbortController()
270+
271+
try {
272+
const timeoutId = setTimeout(() => controller.abort(), 1000)
273+
274+
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`)
275+
276+
clearTimeout(timeoutId)
277+
278+
if (!res.ok) false
279+
280+
const data = await res.text()
281+
return data.split('/\r?\n/').some((line) => line.includes(suffix))
282+
} catch (error) {
283+
if (error instanceof DOMException && error.name === 'AbortError') {
284+
console.warn('Password check timed out')
285+
}
286+
return false
287+
}
288+
}

tests/mocks/common-password.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { http, HttpResponse } from 'msw'
2+
3+
export const pwnedPasswordApiHandler = http.get(
4+
'https://api.pwnedpasswords.com/range/:prefix',
5+
() => {
6+
return new HttpResponse('', { status: 200 })
7+
},
8+
)

tests/mocks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import closeWithGrace from 'close-with-grace'
22
import { setupServer } from 'msw/node'
3+
import { pwnedPasswordApiHandler } from './common-password.ts'
34
import { handlers as githubHandlers } from './github.ts'
45
import { handlers as resendHandlers } from './resend.ts'
56
import { handlers as tigrisHandlers } from './tigris.ts'
@@ -8,6 +9,7 @@ export const server = setupServer(
89
...resendHandlers,
910
...githubHandlers,
1011
...tigrisHandlers,
12+
pwnedPasswordApiHandler,
1113
)
1214

1315
server.listen({

0 commit comments

Comments
 (0)