DEV Community

Adel
Adel

Posted on

CSRF Protection in Next.js

Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to submit a request to a Web application against which they are currently authenticated.

It ensures the authenticity of your requests.

We will use a popular npm package to handle CSRF called csurf.

Because csurf is express middleware, and there is no easy way to include express middlewares in next.js applications we have two options.

1- Create custom express server and use the middleware, check this link
2- Connect express middleware, we will follow this method, more details in next.js docs

we will create new file /src/csrf.js

import csurf from 'csurf'

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
export function csrf(req, res) {
    return new Promise((resolve, reject) => {
        csurf({ cookie: true })(req, res, (result) => {
            if (result instanceof Error) {
                return reject(result)
            }
            return resolve(result)
        })
    })
}

export default csrf
Enter fullscreen mode Exit fullscreen mode

Now we have two steps to implement this,

1- Make sure the API is protected by CSRF token.

Lets take the default API route that comes with initial next.js project "hello.js", to include the middleware we need to do the following

import csrf from "../../src/csrf";
export default async function handler(req, res) {
  await csrf(req, res);
  res.status(200).json({ name: 'John Doe' })
}
Enter fullscreen mode Exit fullscreen mode

This way we are protecting this route with CSRF token

2- Expose this token to the react page so it can be sent with the requests.

To get the token

export async function getServerSideProps(context) {
    const { req, res } = context
    await csrf(req, res)
    return {
        props: { csrfToken: req.csrfToken() },
    }
}
Enter fullscreen mode Exit fullscreen mode

Now on the next API call to hello.js we need to include the token in the header, here I used axios but you can use fetch as well

axios.post('http://localhost:3000/api/hello', {}, {headers:{'CSRF-Token': csrfToken}})
    .then(res=>console.log({data: res.data}))
Enter fullscreen mode Exit fullscreen mode

And that's it, Now you are protected against CSRF attacks

Note that you can add more options to your cookie like make it HttpOnly and change the key name, check the library docs for more details.

Top comments (10)

Collapse
 
dejvo profile image
David Rhoderick • Edited

So I'm trying this in my current project and I'm unable to get it to work. Are you using any other dependencies?

I first tried to implement it based on my project, which utilizes RTK Query, but then I just thought I'd re-add the hello.js endpoint and copy and paste your code exactly and it just gives me an "invalid csrf token" error.

UPDATE: I was sure it had something to do with my project so I did "create next app" and copied and pasted your tutorial in there but it just gives me a 500 error when I try to hit the home page. I'm thinking there's something missing OR maybe versions may be off.

Collapse
 
adelhamad profile image
Adel

just pushed the working example to github.com/adelhamad/nextjs-csrf

I also added try catch to hello.js if you want to customize the response and not just have 500 error

Collapse
 
dejvo profile image
David Rhoderick

Thanks for adding that example, it certainly helps a lot. However, when I run it locally, this is what I get:

u.pcloud.link/publink/show?code=XZ...

Looks like it's not working because I assume one of those buttons should have a different response than the other. Is that CORS error in the console an issue? Could it be a browser-related issue (I'm on Firefox)?

Thread Thread
 
adelhamad profile image
Adel

I don't think it's browser issue, I see in the video you are running the app on port 3001, and the axios call is going to 'localhost:3000/api/hello' which are different origins

So I suggest either run the app on port 3000 or change the request url to port 3001 in pages/index.js

Thread Thread
 
dejvo profile image
David Rhoderick

Bingo that was it! Thanks for the tutorial. Hopefully I can get it working with my project.

Thread Thread
 
kishorgembali profile image
Kishor Gembali

Thanks Adel. It is really helpful.

@dejvo - I am going to implement this as part of a POC, are you able to make it work. Just would like to know if you come across any other challenges.

Thread Thread
 
dejvo profile image
David Rhoderick

@kishorgembali I couldn't get the await csrf(); lines to work. Instead, I had to chain the middleware, but that could be an issue with our specific containerized deployment.

In the end, we also decided against double submit because it seemed the Next.js API routes were always directly accessible even then from any browser so we didn't want that.

We went with the synchronizer token pattern in the end. To achieve that, we basically use the Next.js server to hold the CSRF secret and then the middleware just confirms the CSRF token against that.

Thread Thread
 
kishorgembali profile image
Kishor Gembali

ohk. Got it. Thank you @dejvo. Is it possible to share any reference or sample implementation, I am new to Next JS, dont want to reinvent the wheel. Just checking if it is something available. Appreciate your quick response.

Thread Thread
 
dejvo profile image
David Rhoderick

Basically the way the CSRF is implemented is like this using the csrf NPM module:

import Tokens from 'csrf';
import { GlobalRef } from './global-ref';

const tokens = new Tokens();
const secret = new GlobalRef('domain.csrfSecret');

export const createSecret = () => {
  secret.value = tokens.secretSync();
};

export const resetSecret = () => {
  secret.value = undefined;
};

export const createToken = () => {
  if (!secret.value) {
    return new Error('No secret set');
  }

  return tokens.create(secret.value as string);
};

export const verifyToken = (csrfToken: string) => {
  if (!secret.value) {
    return new Error('No secret set');
  }

  return tokens.verify(secret.value as string, csrfToken);
};
Enter fullscreen mode Exit fullscreen mode

It uses this GlobalRef hack I found online and tweaked to store the CSRF secret:

export class GlobalRef<T> {
  private readonly sym: symbol;

  constructor(uniqueName: string) {
    this.sym = Symbol.for(uniqueName);
  }

  get value() {
    return (global as never)[this.sym] as T;
  }

  set value(value: T) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    (global as any)[this.sym] = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can use those functions in your API endpoints in Next.js. Like I said, I had trouble implementing the middleware how it is in the documentation so I used a callback like this:

export const verifyCsrf = async (
  req: NextApiRequest,
  res: NextApiResponse,
  callback
) => {
  if (req.headers['csrf-token']) {
    if (verifyCsrfToken(req.headers['csrf-token'].toString())) {
      await callback();
    } else {
      console.error('No permissions to perform the request');

      res.status(403).json({
        code: 'access_denied',
        message: 'No permissions to perform the request',
      });
    }
  } else {
      console.error('No permissions to perform the request');

      res.status(403).json({
        code: 'access_denied',
        message: 'No permissions to perform the request',
      });
  }
};
Enter fullscreen mode Exit fullscreen mode

And then call it like this in an endpoint:

export default async function someEndpoint(
  req: NextApiRequest,
  res: NextApiResponse
) {
  await verifyCsrf(req, res, async () => {
      const data = doSomething();
      res.status(200).json(data);
    });
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
gitantonyuk profile image
Alexander Antonyuk

csurf package deprecated due to the large influx of security vulnerability reports received. github.com/expressjs/csurf