Skip to content

Cross Site Request Forgery (CSRF)

Now that we have routing set up, we need a way to secure our application.

We will start by adding cross-site request forgery CSRF protection to our application.

One way to protect against CSRF attacks is to use a CSRF token.

This is a unique string of random characters that the app generates for each user session. Every time a user makes a request to the server, the token is sent along with it.

Before processing any request, the server checks that the token is valid and that it matches the user’s session. If it doesn’t, the request is rejected.

Let’s add this now.

Luckily, adding CSRF protection to our application is relatively easy.

  1. Open app/root.tsx.

  2. Add the following imports to the top of your app/root.tsx file:

    app/root.tsx
    import { Outlet, useLoaderData } from 'react-router'
    import { ParallaxProvider } from 'react-scroll-parallax'
    import { type Route } from './+types/root.ts'
    import { type loader } from './__root.server.tsx'
    import { GeneralErrorBoundary } from './components/error-boundary.tsx'
    import FooterMenuRight from './components/organisms/Footer/FooterMenuRight'
    import HeaderWithSearch from './components/organisms/HeaderWithSearch'
    import Document from './components/shared-layout/Document.tsx'
    import { ThemeSwitch, useTheme } from './routes/resources+/theme-switch.tsx'
    import { useNonce } from './utils/nonce-provider.ts'
    import rootLinkElements from './utils/providers/rootLinkElements.ts'
    import { AuthenticityTokenProvider } from 'remix-utils/csrf/react'
    import { EpicToaster } from './components/ui/sonner.tsx'
    import { useToast } from './components/toaster.tsx'
  3. Wrap all the JSX returned by the App component with the AuthenticityTokenProvider, then pass a csrfToken from the data object to it:

    app/root.tsx
    export default function App() {
    const data = useLoaderData<typeof loader | null>()
    const nonce = useNonce()
    const theme = useTheme()
    return (
    <AuthenticityTokenProvider token={data?.csrfToken ?? ''}>
    <ParallaxProvider>
    <Document theme={theme} nonce={nonce} honeyProps={data?.honeyProps}>
    <div className="flex h-screen flex-col justify-between">
    <HeaderWithSearch />
    <div className="flex-1">
    <Outlet />
    </div>
    <div className="container flex justify-between pb-5">
    <ThemeSwitch userPreference={data?.requestInfo.userPrefs.theme} />
    </div>
    <FooterMenuRight />
    </div>
    </Document>
    </ParallaxProvider>
    </AuthenticityTokenProvider>
    )
    }
  4. Next, add a new component called EpicToaster carefully in the space shown below. Add it just above the closing </Document> tag:

    app/root.tsx
    export default function App() {
    const data = useLoaderData<typeof loader | null>()
    const nonce = useNonce()
    const theme = useTheme()
    useToast(data?.toast)
    return (
    <AuthenticityTokenProvider token={data?.csrfToken ?? ''}>
    <ParallaxProvider>
    <Document theme={theme} nonce={nonce} honeyProps={data?.honeyProps}>
    <div className="flex h-screen flex-col justify-between">
    <HeaderWithSearch />
    <div className="flex-1">
    <Outlet />
    </div>
    <div className="container flex justify-between pb-5">
    <ThemeSwitch userPreference={data?.requestInfo.userPrefs.theme} />
    </div>
    <FooterMenuRight />
    </div>
    <EpicToaster
    closeButton
    position="bottom-right"
    theme={theme}
    expand
    richColors
    duration={5000}
    />
    </Document>
    </ParallaxProvider>
    </AuthenticityTokenProvider>
    )
    }
  5. Save your changes.

  6. Remember to organise the imports at the top of app/root.tsx by using the VS Code keyboard shortcut SHIFT + ALT + O (Windows) or SHIFT + OPT + O (Mac)

  7. Let’s check for a CSRF token when someone tries to login to the website. If they try to login without a valid CSRF token, we will redirect them to the home page and display a toast notification to inform the user.

    Open the file app/routes/_auth+/login.tsx.

  8. Add the following imports to the top of the file:

    app/routes/_auth+/login.tsx
    19 collapsed lines
    import { getFormProps, getInputProps, useForm } from '@conform-to/react'
    import { getZodConstraint, parseWithZod } from '@conform-to/zod'
    import { type SEOHandle } from '@nasa-gcn/remix-seo'
    import { startAuthentication } from '@simplewebauthn/browser'
    import { useOptimistic, useState, useTransition } from 'react'
    import { data, Form, Link, useNavigate, useSearchParams } from 'react-router'
    import { HoneypotInputs } from 'remix-utils/honeypot/react'
    import { z } from 'zod'
    import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
    import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
    import { Spacer } from '#app/components/spacer.tsx'
    import { Icon } from '#app/components/ui/icon.tsx'
    import { StatusButton } from '#app/components/ui/status-button.tsx'
    import { login, requireAnonymous } from '#app/utils/auth.server.ts'
    import {
    ProviderConnectionForm,
    providerNames,
    } from '#app/utils/connections.tsx'
    import { checkHoneypot } from '#app/utils/honeypot.server.ts'
    import { getErrorMessage, useIsPending } from '#app/utils/misc.tsx'
    import { PasswordSchema, UsernameSchema } from '#app/utils/user-validation.ts'
    import { type Route } from './+types/login.ts'
    import { handleNewSession } from './login.server.ts'
    import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
    import { CSRFError } from 'remix-utils/csrf/server'
    import { csrf } from '~/utils/csrf.server'
    import { redirectWithToast } from '#app/utils/toast.server.ts'
  9. Next, find the action function around line 52 in the same file.

    Add the following block of code just after its opening bracket:

    app/routes/_auth+/login.tsx
    export async function action({ request }: Route.ActionArgs) {
    try {
    await csrf.validate(request)
    } catch (error) {
    if (error instanceof CSRFError) {
    return redirectWithToast(`/`, {
    description: 'CSRF token is invalid. Please try again in a new window',
    type: 'error',
    })
    }
    return redirectWithToast(`/`, {
    description: 'Something went wrong. Please try again in a new window',
    type: 'error',
    })
    }
    await requireAnonymous(request)
    const formData = await request.formData()
    await checkHoneypot(formData)
    const submission = await parseWithZod(formData, {
    33 collapsed lines
    schema: (intent) =>
    LoginFormSchema.transform(async (data, ctx) => {
    if (intent !== null) return { ...data, session: null }
    const session = await login(data)
    if (!session) {
    ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: 'Invalid username or password',
    })
    return z.NEVER
    }
    return { ...data, session }
    }),
    async: true,
    })
    if (submission.status !== 'success' || !submission.value.session) {
    return data(
    { result: submission.reply({ hideFields: ['password'] }) },
    { status: submission.status === 'error' ? 400 : 200 },
    )
    }
    const { session, remember, redirectTo } = submission.value
    return handleNewSession({
    request,
    session,
    remember: remember ?? false,
    redirectTo,
    })
    }
  10. Find the LoginPage component around line 104 in the same file. Look carefully for the Form element in the JSX being returned from this function, and add in the AuthenticityTokenInput component just below the HoneypotInputs component:

    app/routes/_auth+/login.tsx
    export default function LoginPage({ actionData }: Route.ComponentProps) {
    14 collapsed lines
    const isPending = useIsPending()
    const [searchParams] = useSearchParams()
    const redirectTo = searchParams.get('redirectTo')
    const [form, fields] = useForm({
    id: 'login-form',
    constraint: getZodConstraint(LoginFormSchema),
    defaultValue: { redirectTo },
    lastResult: actionData?.result,
    onValidate({ formData }) {
    return parseWithZod(formData, { schema: LoginFormSchema })
    },
    shouldRevalidate: 'onBlur',
    })
    return (
    <div className="flex min-h-full flex-col justify-center pt-20 pb-32">
    <div className="mx-auto w-full max-w-md">
    8 collapsed lines
    <div className="flex flex-col gap-3 text-center">
    <h1 className="text-h1">Welcome back!</h1>
    <p className="text-body-md text-muted-foreground">
    Please enter your details.
    </p>
    </div>
    <Spacer size="xs" />
    <div>
    <div className="mx-auto w-full max-w-md px-8">
    <Form method="POST" {...getFormProps(form)}>
    <AuthenticityTokenInput />
    <HoneypotInputs />
    <Field
    labelProps={{ children: 'Username' }}
    inputProps={{
    ...getInputProps(fields.username, { type: 'text' }),
    autoFocus: true,
    className: 'lowercase',
    autoComplete: 'username',
    }}
    errors={fields.username.errors}
    />
    48 collapsed lines
    <Field
    labelProps={{ children: 'Password' }}
    inputProps={{
    ...getInputProps(fields.password, {
    type: 'password',
    }),
    autoComplete: 'current-password',
    }}
    errors={fields.password.errors}
    />
    <div className="flex justify-between">
    <CheckboxField
    labelProps={{
    htmlFor: fields.remember.id,
    children: 'Remember me',
    }}
    buttonProps={getInputProps(fields.remember, {
    type: 'checkbox',
    })}
    errors={fields.remember.errors}
    />
    <div>
    <Link
    to="/forgot-password"
    className="text-body-xs font-semibold"
    >
    Forgot password?
    </Link>
    </div>
    </div>
    <input
    {...getInputProps(fields.redirectTo, { type: 'hidden' })}
    />
    <ErrorList errors={form.errors} id={form.errorId} />
    <div className="flex items-center justify-between gap-6 pt-3">
    <StatusButton
    className="w-full"
    status={isPending ? 'pending' : (form.status ?? 'idle')}
    type="submit"
    disabled={isPending}
    >
    Log in
    </StatusButton>
    </div>
    </Form>
    31 collapsed lines
    <hr className="my-4" />
    <div className="flex flex-col gap-5">
    <PasskeyLogin
    redirectTo={redirectTo}
    remember={fields.remember.value === 'on'}
    />
    </div>
    <hr className="my-4" />
    <ul className="flex flex-col gap-5">
    {providerNames.map((providerName) => (
    <li key={providerName}>
    <ProviderConnectionForm
    type="Login"
    providerName={providerName}
    redirectTo={redirectTo}
    />
    </li>
    ))}
    </ul>
    <div className="flex items-center justify-center gap-2 pt-6">
    <span className="text-muted-foreground">New here?</span>
    <Link
    to={
    redirectTo
    ? `/signup?redirectTo=${encodeURIComponent(redirectTo)}`
    : '/signup'
    }
    >
    Create an account
    </Link>
    </div>
    </div>
    </div>
    </div>
    </div>
    )
    }
  11. Open app/root.tsx.

  12. We will deliberately add an invalid CSRF token to the AuthenticityTokenProvider to test our new code.

    Replace the token prop with the following:

    app/root.tsx
    export default function App() {
    const data = useLoaderData<typeof loader | null>()
    const nonce = useNonce()
    const theme = useTheme()
    useToast(data?.toast)
    return (
    <AuthenticityTokenProvider token={'invalid'}>
    25 collapsed lines
    <ParallaxProvider>
    <Document theme={theme} nonce={nonce} honeyProps={data?.honeyProps}>
    <div className="flex h-screen flex-col justify-between">
    <HeaderWithSearch />
    <div className="flex-1">
    <Outlet />
    </div>
    <div className="container flex justify-between pb-5">
    <ThemeSwitch userPreference={data?.requestInfo.userPrefs.theme} />
    </div>
    <FooterMenuRight />
    </div>
    <EpicToaster
    closeButton
    position="bottom-right"
    theme={theme}
    expand
    richColors
    duration={5000}
    />
    </Document>
    </ParallaxProvider>
    </AuthenticityTokenProvider>
    )
    }
  13. Next, in your browser, click on the Login button in the top right corner of the screen.

    Login with the following details:

    Username: kody Password: kodylovesyou

  14. When you click β€˜Login’, you should be redirected to the home page and see a toast notification informing you that the CSRF token is invalid:

    Toast notification example

We’ve added a strong security feature to our application that is crucial for you to write about in your assignment.

In the next step, we will secure our application against spambots by adding a β€˜honeypot’ field to our forms.