Skip to content

Commit

Permalink
🎉 add csrf and honeypot
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Oct 12, 2023
1 parent 605c029 commit b25c01d
Show file tree
Hide file tree
Showing 27 changed files with 245 additions and 24 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DATABASE_PATH="./prisma/data.db"
DATABASE_URL="file:./data.db?connection_limit=1"
CACHE_DATABASE_PATH="./other/cache.db"
SESSION_SECRET="super-duper-s3cret"
HONEYPOT_SECRET="super-duper-s3cret"
INTERNAL_COMMAND_TOKEN="some-made-up-token"
RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh"
SENTRY_DSN="your-dsn"
Expand Down
23 changes: 22 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import { useRef } from 'react'
import { AuthenticityTokenProvider } from 'remix-utils/csrf/react'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { Confetti } from './components/confetti.tsx'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
Expand All @@ -46,8 +48,10 @@ import tailwindStyleSheetUrl from './styles/tailwind.css'
import { authenticator, getUserId } from './utils/auth.server.ts'
import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx'
import { getConfetti } from './utils/confetti.server.ts'
import { csrf } from './utils/csrf.server.ts'
import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
import { honeypot } from './utils/honeypot.server.ts'
import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
import { useRequestInfo } from './utils/request-info.ts'
Expand Down Expand Up @@ -130,6 +134,8 @@ export async function loader({ request }: DataFunctionArgs) {
}
const { toast, headers: toastHeaders } = await getToast(request)
const { confettiId, headers: confettiHeaders } = getConfetti(request)
const honeyProps = honeypot.getInputProps()
const [csrfToken, csrfCookieHeader] = await csrf.commitToken()

return json(
{
Expand All @@ -145,12 +151,15 @@ export async function loader({ request }: DataFunctionArgs) {
ENV: getEnv(),
toast,
confettiId,
honeyProps,
csrfToken,
},
{
headers: combineHeaders(
{ 'Server-Timing': timings.toString() },
toastHeaders,
confettiHeaders,
csrfCookieHeader ? { 'set-cookie': csrfCookieHeader } : null,
),
},
)
Expand Down Expand Up @@ -276,7 +285,19 @@ function App() {
</Document>
)
}
export default withSentry(App)

function AppWithProviders() {
const data = useLoaderData<typeof loader>()
return (
<AuthenticityTokenProvider token={data.csrfToken}>
<HoneypotProvider {...data.honeyProps}>
<App />
</HoneypotProvider>
</AuthenticityTokenProvider>
)
}

export default withSentry(AppWithProviders)

function UserDropdown() {
const user = useUser()
Expand Down
8 changes: 8 additions & 0 deletions app/routes/_auth+/forgot-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import {
type MetaFunction,
} from '@remix-run/node'
import { Link, useFetcher } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { sendEmail } from '#app/utils/email.server.ts'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { EmailSchema, UsernameSchema } from '#app/utils/user-validation.ts'
import { prepareVerification } from './verify.tsx'

Expand All @@ -23,6 +27,8 @@ const ForgotPasswordSchema = z.object({

export async function action({ request }: DataFunctionArgs) {
const formData = await request.formData()
await validateCSRF(formData, request.headers)
checkHoneypot(formData)
const submission = await parse(formData, {
schema: ForgotPasswordSchema.superRefine(async (data, ctx) => {
const user = await prisma.user.findFirst({
Expand Down Expand Up @@ -136,6 +142,8 @@ export default function ForgotPasswordRoute() {
</div>
<div className="mx-auto mt-16 min-w-[368px] max-w-sm">
<forgotPassword.Form method="POST" {...form.props}>
<AuthenticityTokenInput />
<HoneypotInputs />
<div>
<Field
labelProps={{
Expand Down
8 changes: 8 additions & 0 deletions app/routes/_auth+/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
type MetaFunction,
} from '@remix-run/node'
import { Form, Link, useActionData, useSearchParams } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { safeRedirect } from 'remix-utils/safe-redirect'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
Expand All @@ -24,7 +26,9 @@ import {
ProviderConnectionForm,
providerNames,
} from '#app/utils/connections.tsx'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import {
combineResponseInits,
invariant,
Expand Down Expand Up @@ -195,6 +199,8 @@ export async function loader({ request }: DataFunctionArgs) {
export async function action({ request }: DataFunctionArgs) {
await requireAnonymous(request)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
checkHoneypot(formData)
const submission = await parse(formData, {
schema: intent =>
LoginFormSchema.transform(async (data, ctx) => {
Expand Down Expand Up @@ -266,6 +272,8 @@ export default function LoginPage() {
<div>
<div className="mx-auto w-full max-w-md px-8">
<Form method="POST" {...form.props}>
<AuthenticityTokenInput />
<HoneypotInputs />
<Field
labelProps={{ children: 'Username' }}
inputProps={{
Expand Down
8 changes: 8 additions & 0 deletions app/routes/_auth+/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ import {
useLoaderData,
useSearchParams,
} from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { safeRedirect } from 'remix-utils/safe-redirect'
import { z } from 'zod'
import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
import { Spacer } from '#app/components/spacer.tsx'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { requireAnonymous, sessionKey, signup } from '#app/utils/auth.server.ts'
import { redirectWithConfetti } from '#app/utils/confetti.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { invariant, useIsPending } from '#app/utils/misc.tsx'
import { authSessionStorage } from '#app/utils/session.server.ts'
import {
Expand Down Expand Up @@ -64,6 +68,8 @@ export async function loader({ request }: DataFunctionArgs) {
export async function action({ request }: DataFunctionArgs) {
const email = await requireOnboardingEmail(request)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
checkHoneypot(formData)
const submission = await parse(formData, {
schema: intent =>
SignupFormSchema.superRefine(async (data, ctx) => {
Expand Down Expand Up @@ -165,6 +171,8 @@ export default function SignupRoute() {
className="mx-auto min-w-[368px] max-w-sm"
{...form.props}
>
<AuthenticityTokenInput />
<HoneypotInputs />
<Field
labelProps={{ htmlFor: fields.username.id, children: 'Username' }}
inputProps={{
Expand Down
5 changes: 1 addition & 4 deletions app/routes/_auth+/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import { type VerifyFunctionArgs } from './verify.tsx'

const resetPasswordUsernameSessionKey = 'resetPasswordUsername'

export async function handleVerification({
request,
submission,
}: VerifyFunctionArgs) {
export async function handleVerification({ submission }: VerifyFunctionArgs) {
invariant(submission.value, 'submission.value should be defined by now')
const target = submission.value.target
const user = await prisma.user.findFirst({
Expand Down
10 changes: 10 additions & 0 deletions app/routes/_auth+/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
type MetaFunction,
} from '@remix-run/node'
import { Form, useActionData, useSearchParams } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList, Field } from '#app/components/forms.tsx'
Expand All @@ -16,8 +18,10 @@ import {
ProviderConnectionForm,
providerNames,
} from '#app/utils/connections.tsx'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { sendEmail } from '#app/utils/email.server.ts'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
import { EmailSchema } from '#app/utils/user-validation.ts'
import { prepareVerification } from './verify.tsx'
Expand All @@ -28,6 +32,10 @@ const SignupSchema = z.object({

export async function action({ request }: DataFunctionArgs) {
const formData = await request.formData()

await validateCSRF(formData, request.headers)
checkHoneypot(formData)

const submission = await parse(formData, {
schema: SignupSchema.superRefine(async (data, ctx) => {
const existingUser = await prisma.user.findUnique({
Expand Down Expand Up @@ -131,6 +139,8 @@ export default function SignupRoute() {
</div>
<div className="mx-auto mt-16 min-w-[368px] max-w-sm">
<Form method="POST" {...form.props}>
<AuthenticityTokenInput />
<HoneypotInputs />
<Field
labelProps={{
htmlFor: fields.email.id,
Expand Down
11 changes: 10 additions & 1 deletion app/routes/_auth+/verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { generateTOTP, verifyTOTP } from '@epic-web/totp'
import { json, type DataFunctionArgs } from '@remix-run/node'
import { Form, useActionData, useSearchParams } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList, Field } from '#app/components/forms.tsx'
Expand All @@ -12,7 +14,9 @@ import { handleVerification as handleChangeEmailVerification } from '#app/routes
import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
import { type twoFAVerifyVerificationType } from '#app/routes/settings+/profile.two-factor.verify.tsx'
import { requireUserId } from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { ensurePrimary } from '#app/utils/litefs.server.ts'
import { getDomainUrl, useIsPending } from '#app/utils/misc.tsx'
import { redirectWithToast } from '#app/utils/toast.server.ts'
Expand All @@ -39,7 +43,10 @@ const VerifySchema = z.object({
})

export async function action({ request }: DataFunctionArgs) {
return validateRequest(request, await request.formData())
const formData = await request.formData()
checkHoneypot(formData)
await validateCSRF(formData, request.headers)
return validateRequest(request, formData)
}

export function getRedirectToUrl({
Expand Down Expand Up @@ -277,6 +284,8 @@ export default function VerifyRoute() {
</div>
<div className="flex w-full gap-2">
<Form method="POST" {...form.props} className="flex-1">
<AuthenticityTokenInput />
<HoneypotInputs />
<Field
labelProps={{
htmlFor: fields[codeQueryParam].id,
Expand Down
4 changes: 4 additions & 0 deletions app/routes/settings+/profile.change-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type SEOHandle } from '@nasa-gcn/remix-seo'
import * as E from '@react-email/components'
import { json, redirect, type DataFunctionArgs } from '@remix-run/node'
import { Form, useActionData, useLoaderData } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
Expand All @@ -14,6 +15,7 @@ import {
type VerifyFunctionArgs,
} from '#app/routes/_auth+/verify.tsx'
import { requireUserId } from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { sendEmail } from '#app/utils/email.server.ts'
import { invariant, useIsPending } from '#app/utils/misc.tsx'
Expand Down Expand Up @@ -98,6 +100,7 @@ export async function loader({ request }: DataFunctionArgs) {
export async function action({ request }: DataFunctionArgs) {
const userId = await requireUserId(request)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
const submission = await parse(formData, {
schema: ChangeEmailSchema.superRefine(async (data, ctx) => {
const existingUser = await prisma.user.findUnique({
Expand Down Expand Up @@ -225,6 +228,7 @@ export default function ChangeEmailIndex() {
</p>
<div className="mx-auto mt-5 max-w-sm">
<Form method="POST" {...form.props}>
<AuthenticityTokenInput />
<Field
labelProps={{ children: 'New Email' }}
inputProps={conform.input(fields.email)}
Expand Down
6 changes: 6 additions & 0 deletions app/routes/settings+/profile.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { json, type DataFunctionArgs } from '@remix-run/node'
import { Link, useFetcher, useLoaderData } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { requireUserId, sessionKey } from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import {
getUserImgSrc,
Expand Down Expand Up @@ -82,6 +84,7 @@ const deleteDataActionIntent = 'delete-data'
export async function action({ request }: DataFunctionArgs) {
const userId = await requireUserId(request)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
const intent = formData.get('intent')
switch (intent) {
case profileUpdateActionIntent: {
Expand Down Expand Up @@ -234,6 +237,7 @@ function UpdateProfile() {

return (
<fetcher.Form method="POST" {...form.props}>
<AuthenticityTokenInput />
<div className="grid grid-cols-6 gap-x-10">
<Field
className="col-span-3"
Expand Down Expand Up @@ -301,6 +305,7 @@ function SignOutOfSessions() {
<div>
{otherSessionsCount ? (
<fetcher.Form method="POST">
<AuthenticityTokenInput />
<StatusButton
{...dc.getButtonProps({
type: 'submit',
Expand Down Expand Up @@ -344,6 +349,7 @@ function DeleteData() {
return (
<div>
<fetcher.Form method="POST">
<AuthenticityTokenInput />
<StatusButton
{...dc.getButtonProps({
type: 'submit',
Expand Down
4 changes: 4 additions & 0 deletions app/routes/settings+/profile.password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { json, redirect, type DataFunctionArgs } from '@remix-run/node'
import { Form, Link, useActionData } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand All @@ -13,6 +14,7 @@ import {
requireUserId,
verifyUserPassword,
} from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
import { redirectWithToast } from '#app/utils/toast.server.ts'
Expand Down Expand Up @@ -60,6 +62,7 @@ export async function action({ request }: DataFunctionArgs) {
const userId = await requireUserId(request)
await requirePassword(userId)
const formData = await request.formData()
await validateCSRF(formData, request.headers)
const submission = await parse(formData, {
async: true,
schema: ChangePasswordForm.superRefine(
Expand Down Expand Up @@ -129,6 +132,7 @@ export default function ChangePasswordRoute() {

return (
<Form method="POST" {...form.props} className="mx-auto max-w-md">
<AuthenticityTokenInput />
<Field
labelProps={{ children: 'Current Password' }}
inputProps={conform.input(fields.currentPassword, { type: 'password' })}
Expand Down
Loading

0 comments on commit b25c01d

Please sign in to comment.