nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.95k stars 3.52k forks source link

Verify csrf-token #717

Closed PaulKushch closed 4 years ago

PaulKushch commented 4 years ago

Your question Hi, I wonder how to validate csrf-token in a custom post request to custom route?

What are you trying to do I add csrf-token as a header in fetch:


    fetch("/api/test", {
      method: "POST",
      headers: {
        "Content-type": "application/json; charset=UTF-8",
        "X-CSRF-TOKEN": csrfToken,
      },
      body: JSON.stringify(data),
    })
      .then((res) => res.json())
      .then((json) => console.log(json.user));

And i can see it is present in headers on `/api/test`:
`'x-csrf-token': 'ea5930568fb6c99e04d9a82c028ffacc0fb89570c56ede536e47ddadd0fbf0cf',`

But how should one validate that csrf-token is valid?

``'
chrishornmem commented 4 years ago

I have the same question. Is it enough to move the route to /api/auth/test ?

It seems from looking at the code and documentation that only /api/auth/signin and /api/auth/signout routes are checked for a valid csrf token.

https://github.com/nextauthjs/next-auth/blob/main/src/server/index.js

CRGavrila commented 4 years ago

Any update?

iaincollins commented 4 years ago

All HTTP POST requests are checked for a valid CSRF token (e.g. sign in, sign out).

NextAuth.js implements CSRF tokens but is not itself an CSRF token implementation for the rest of your site.

It's technically possible to use the token created by NextAuth.js elsewhere in your app, but a little complicated to explain due to caveats (to support things like cookie prefixes, secret values, etc vary between prod and dev environments).

The code for how NextAuth.js verifies the token is at https://github.com/nextauthjs/next-auth/blob/main/src/server/index.js#L146 and you could use that as a reference to implement a method (it's not terribly complex, but you can get an idea of the issues).

If folks could would find it useful to have an API function to allow tokens to be easily verified, that would make a good feature request.

chrishornmem commented 4 years ago

I'm putting here what I came up with for someone to check/comment. It seems to work.

import { createHash } from 'crypto';
import { parseUrl } from '../../../utils'; // see https://github.com/nextauthjs/next-auth/blob/main/src/lib/parse-url.js

const secret = process.env.SECRET;

async function verifyCsrfToken({req}) {

  const csrfMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];

  try {
    const { body } = req
    const {
      csrfToken: csrfTokenFromBody
    } = body;

    const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL);
    const baseUrl = parsedUrl.baseUrl;
    const useSecureCookies = baseUrl.startsWith('https://')
    const csrfProp = `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`;

    if (req.cookies[csrfProp]) {
      const [csrfTokenValue, csrfTokenHash] = req.cookies[csrfProp].split('|');
      if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
        // If hash matches then we trust the CSRF token value  
        // If this is a method that is allowed to use csrf protection and the CSRF Token in the request body matches
        // the cookie we have already verified is one we have set, then token is verified!
        if (csrfMethods.includes(req.method) && csrfTokenValue === csrfTokenFromBody) return true;
      }
    }
    return false;
  } catch (error) {
    console.log(error);
    return false;
  }
}

Use like this, eg in an API route:

export default async (req, res) => {
    const isCsrfValid = await verifyCsrfToken({req});
    ...
  }
}
pandydavey commented 3 years ago

Hi @chrishornmem i've taken what you have an changed it a little bit. The core is there, just a few tweaks really

import { createHash } from 'crypto';
import { NextApiRequest } from 'next';

/**
 * Verify that the token you want to check matches the token in the next-auth cookie
 *
 * Note this verify check has been created based on the code within next-auth: ^3.1.0 future
 * versions might differ
 *
 * @param req
 * @param tokenToCheck
 * @return boolean
 */
const verifyNextAuthCsrfToken = (req: NextApiRequest, tokenToCheck: string) => {

    const secret: string = process.env.APP_SECRET_STRING;
    const csrfMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];

    if(!csrfMethods.includes(req.method)) {
        // we dont need to check the CSRF if it's not within the method.
        return true;
    }

    try {

        const useSecureCookies = process.env.NEXTAUTH_URL.startsWith('https://')
        const csrfProp = `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`;

        if (req.cookies[csrfProp]) {
            const cookieValue = req.cookies[csrfProp];
            const cookieSplitKey = cookieValue.match("|") ? "|" : "%7C";

            const [csrfTokenValue, csrfTokenHash] = cookieValue.split(cookieSplitKey);

            const generatedHash = createHash('sha256').update(`${tokenToCheck}${secret}`).digest('hex');

            if (csrfTokenHash === generatedHash) {
                // If hash matches then we trust the CSRF token value
                if (csrfTokenValue === tokenToCheck) return true;
            }
        }
        return false;
    } catch (error) {
        return false;
    }
}

export default verifyNextAuthCsrfToken;
pandydavey commented 3 years ago

For those wondering: const cookieSplitKey = cookieValue.match("|") ? "|" : "%7C"; is there because the value seems to be URL encoded on Vercel.

pandydavey commented 3 years ago

Oh and dont forget that:

const secret: string = process.env.APP_SECRET_STRING;

needs to be the same value as the secret sent to NextAuth setOptions

const getOptions = () => {
    const options = {
      ...
      secret: process.env.APP_SECRET_STRING,
      ...
   }
};
pandydavey commented 3 years ago

ok, final update here. I've just been testing this on vercel and i had some issues. Turns out that the url i store as an env (which is the vercel_url) doesnt include the protocol. So you can do one of two things i reckon.

  1. Check the vercel env (process.env.VERCEL_ENV) and ensure you arent local (unless local is ssl)
  2. Add a new env variable with true value on prod / preview for isSSL. So something like process.env.SSL

Then you can update this line:

const useSecureCookies = process.env.NEXTAUTH_URL.startsWith('https://')

to be something like:

const useSecureCookies = process.env.SSL;

or

const useSecureCookies = process.env.VERCEL_ENV !== "development";

jayliew commented 3 years ago

I wanted to share a bare-bones proof-of-concept of how to secure an API route using the CSRF token created by NextAuth:

import { createHash } from 'crypto'

export default function handler(req, res) {
  try{
    if(!req.headers.cookie){
      res.status(403).json({ status: 'no cookie?'})
    }

    const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies
    const rawCookiesArr = rawCookieString.split(';')

    let parsedCsrfTokenAndHash = null

    for(let i=0; i<rawCookiesArr.length; i++){ // loop through cookies to find CSRF from next-auth
      let cookieArr = rawCookiesArr[i].split('=')
      if(cookieArr[0].trim() === 'next-auth.csrf-token'){
        parsedCsrfTokenAndHash = cookieArr[1]      
        break
      }
    }

    if(!parsedCsrfTokenAndHash){
      res.status(403).json({ status: 'missing csrf'}) // can't find next-auth CSRF in cookies
    }

    // delimiter could be either a '|' or a '%7C'
    const tokenHashDelimiter = parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'

    const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(tokenHashDelimiter)

    const secret = process.env.SECRET

    // compute the valid hash
    const validHash = createHash('sha256').update(`${requestToken}${secret}`).digest('hex')

    if(requestHash !== validHash){
      res.status(403).json({ status: 'bad hash'}) // bad hash
    }
  }catch(err){
    res.status(500).json({ status: 'catch-all no'})
  }

  // otherwise, if everything checks out ..
  let responseOutput = { status: 'success' }
  if(req.body){
    // how to access data sent by client
    responseOutput['received_data'] = req.body
  }
  res.status(200).json(responseOutput)
}
typedashutosh commented 3 years ago

@jayliew can you explain last 8 lines. It throws error on my editor

image

edit: Got it, just a Typescript error. this would work fine

  // otherwise, if everything checks out ..
  let responseOutput: { status: string; received_data?: any } = {
    status: 'success'
  }
  if (req.body) {
    // how to access data sent by client
    responseOutput['received_data'] = req.body // ??? find out this
  }
  res.status(200).json(responseOutput)
}
typedashutosh commented 3 years ago

@jayliew I found your codes working with little changes but I found two things worth mentioning, to make it practically usable.

This would only work if you have process.env.SECRET in your .env.local file, and secret: process.env.SECRET in your NexxtAuthOptions. The default secret generation won't let it pass through this.

I'm using this code:


// ./middlewares/csrf_validator.ts

import { createHash } from 'crypto'
import { NextApiRequest, NextApiResponse } from 'next'

export default (req: NextApiRequest, res: NextApiResponse): boolean => {
  try {
    if (!req.headers.cookie) {
      res.status(403).json({ error: { status: 'no cookie?' } })
      return false
    } else {
      const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies
      const rawCookiesArr = rawCookieString.split(';')

      let parsedCsrfTokenAndHash = null

      for (let i = 0; i < rawCookiesArr.length; i++) {
        // loop through cookies to find CSRF from next-auth
        let cookieArr = rawCookiesArr[i].split('=')
        if (cookieArr[0].trim().search('next-auth.csrf-token')) {   
         // because on vercel, token is named _Host-next-auth.csrf-token
          parsedCsrfTokenAndHash = cookieArr[1]
          break
        }
      }

      if (!parsedCsrfTokenAndHash) {
        res.status(403).json({ error: { status: 'missing csrf' } }) 
        // can't find next-auth CSRF in cookies
        return false
      } else {
        // delimiter could be either a '|' or a '%7C'
        const tokenHashDelimiter =
          parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'

        const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(
          tokenHashDelimiter
        )

        const secret = process.env.SECRET

        // compute the valid hash
        const validHash = createHash('sha256')
          .update(`${requestToken}${secret}`)
          .digest('hex')
        if (requestHash !== validHash) {
          res.status(403).json({ error: { status: 'bad hash' } }) // bad hash
          return false
        }
      }
    }
  } catch (err) {
    res.status(500).json({ error: { status: 'catch-all no' } })
    return false
  }
  return true
}

// ./pages/api/new_user.ts

//...codes
    if (!csrf_validator(req, res)) return 
// if token failed, it won't run codes next, otherwise there would be [ERR_HTTP_HEADERS_SENT] error
// if token passed it will return true, then you can run rest of your codes.
// this could also be achieved by checking for res.headerSent() which returns boolean
//...codes
hbinduni commented 3 years ago

i try to validate csrf too, but i found authorize credentials, only sent the first part of csrf token along with username/password. something like this:

credentials: {
  "csrfToken":"1895edb24f50f2b2accb7fb51a9702143cac20b81efa025039ad14869eca2fb1",
  "username":"jokowi",
  "password":"passwd"
}

from the explanation above, validation should be taking from hashing the first part of csrf token + secret, compare to second part of the scrf. like below:

if (hashing(first_part_csrf + secret) !== second_part_scrf) return not valid
return valid

how can we validate the csrf only using the first part? i know we can get the full csrf from request raw header on authorize credential, but maybe there is another more proper way to do this?

balazsorban44 commented 3 years ago

You don't have to validate the CSRF token yourself, we do that already! :)

https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/csrf-token-handler.js

So when the authorize callback is fired, you should already be good :wink:

ilijaNL commented 3 years ago

I think this should be documented since documentation on https://next-auth.js.org/tutorials/securing-pages-and-api-routes gives an impression that the api routes are also secured by CSRF protection but this is not the case.

balazsorban44 commented 3 years ago

Where do you get that impression? I checked the page you linked to, I don't see a reference to CSRF throughout. Happy to take any documentation clarification PR if you think something is missing though!

ilijaNL commented 3 years ago

Hmm there is no mention of it, but the header states: "Securing API Routes", which indeed as far as I understand only validate the jwt session cookie. Perhaps a note should be added that this is only using the session cookie and still vulnerable to (csrf etc) attacks.

balazsorban44 commented 3 years ago

Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive/should care about the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like sign-in or sign-out, but for securing APIs, I'm not aware you would need CSRF protection.

I think there is a misunderstanding in the purpose of CSRF tokens: https://stackoverflow.com/a/10741650

Also, this discussion might be relevant maybe?: #2381

In any case, please open a PR with your suggestions and I'll discuss it with the others!

chrishornmem commented 3 years ago

The use case is form submission to a custom api route using eg post with csrf token in body params.

On 16 Jul 2021, at 22:40, Balázs Orbán @.***> wrote:

 Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like signin or signOut, but for securing APIs, I'm not aware you would need CSRF protection.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

christopherliedtke commented 2 years ago

Hi there, as I am in the process of implementing next-auth to my application and looking for the most efficient way to secure it against csrf attacks, I ended up here.

I would like to strengthen the points of earlier comments.

Where do you get that impression? I checked the page you linked to, I don't see a reference to CSRF throughout. Happy to take any documentation clarification PR if you think something is missing though!

In the documentation is stated "The CSRF token returned by this endpoint must be passed as form variable named csrfToken in all POST submissions to any API endpoint.". I guess that this could be missunderstood as this is correct only for the next-auth API endpoints.

Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive/should care about the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like sign-in or sign-out, but for securing APIs, I'm not aware you would need CSRF protection.

I think there is a misunderstanding in the purpose of CSRF tokens: https://stackoverflow.com/a/10741650

Also, this discussion might be relevant maybe?: #2381

In any case, please open a PR with your suggestions and I'll discuss it with the others!

Which brings me to my actual issue. I think it is very useful that next-auth brings csrf-protection with it. The issue is, that a web application typically has many more API endpoints which are receiving POST requests (mutating data through the backend). Those need to be secured against csrf attacks too. It would be extremely useful if there was a server-side method exposed by next-auth to verify the csrf token for custom api routes to use the solution throughout the entire application. Otherwise it is necessary to integrate an additional csrf mitigation strategy on top of next-auth.

ilijaNL commented 2 years ago

@chrishornmem Exactly. However since my last comment a lot has been changed, I wonder if using unstable_getServerSession also protects against CSRF attacks when using in custom api (post) routes @balazsorban44 ?

AndrewRayCode commented 2 years ago

@typedashutosh @jayliew Is all of that ceremony necessary? The below works for me on Next 12. Also req.cookies is an object which saves a lot of work.

import { getCsrfToken } from 'next-auth/react';
import { NextApiRequest, NextApiResponse } from 'next';

const NEXTAUTH_CSRF_COOKIE_NAME = 'next-auth.csrf-token';

type Handler = (req: NextApiRequest, res: NextApiResponse) => void;

const validateCsrfPost =
  (handler: Handler) => async (req: NextApiRequest, res: NextApiResponse) => {
    if (req.method === 'POST') {
      const token = await getCsrfToken({ req });
      // Bail out if user's csrf cookie doesn't match session token
      if (req.cookies[NEXTAUTH_CSRF_COOKIE_NAME].split('|')?.[0] !== token) {
        res.status(422).send(null);
        return;
      }
    }
    return handler(req, res);
  };

export default validateCsrfPost;

and usage, wrapping an API route:

const action = validateCsrfPost(
  async (req: NextApiRequest, res: NextApiResponse) => {
    ....
  }
);

What I'm unsure of is the security of this test. getCsrfToken depends on the request/user's cookies. So I'm curious if the user directly edits the next-auth.csrf-token or edits their cookie in another way, would that bypass the check?

I tested with EditThisCookie and manually changing my 'next-auth.csrf-token' value. When I do this. getCsrfToken on the server side generates a new csrf token. So I'm guessing there's some kind of signature checking that happens inside of getCsrfToken (i'm guessing the data after the | is the sig?) but would be grateful if @iaincollins or other member could validate if this is a secure approach to reuse NextAuth's csrf token for arbitrary API routes.

Also, nextjs middleware doesn't allow blocking the request nor changing the status code, which I don't understand at all, which is why I went with a higher order handler function.

yanickrochon commented 2 years ago

Why is this not part of Next Auth's public methods is beyond me! Why are every projects who need this in user land must implement their own functions like this one?

// ./pages/api/some-api-endpoint.js
import { /*createCSRFToken,*/ verifyCSRFToken } from 'next-auth/core/lib/csrf-token';

export default function handler(req, res) {
  if (req.method === 'POST') {
    if (!verifyCSRFToken(req.body.csrfToken)) return;

    // ...

  } else {
    res.setHeader('Allow', ['POST'])
    res.status(405).json({ success:false, error:`Method ${req.method} Not Allowed` });
  }
}

I wanted to share a bare-bones proof-of-concept of how to secure an API route using the CSRF token created by NextAuth:

import { createHash } from 'crypto'

export default function handler(req, res) {
  try{
    if(!req.headers.cookie){
      res.status(403).json({ status: 'no cookie?'})
    }

    const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies
    const rawCookiesArr = rawCookieString.split(';')

    let parsedCsrfTokenAndHash = null

    for(let i=0; i<rawCookiesArr.length; i++){ // loop through cookies to find CSRF from next-auth
      let cookieArr = rawCookiesArr[i].split('=')
      if(cookieArr[0].trim() === 'next-auth.csrf-token'){
        parsedCsrfTokenAndHash = cookieArr[1]      
        break
      }
    }

    if(!parsedCsrfTokenAndHash){
      res.status(403).json({ status: 'missing csrf'}) // can't find next-auth CSRF in cookies
    }

    // delimiter could be either a '|' or a '%7C'
    const tokenHashDelimiter = parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'

    const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(tokenHashDelimiter)

    const secret = process.env.SECRET

    // compute the valid hash
    const validHash = createHash('sha256').update(`${requestToken}${secret}`).digest('hex')

    if(requestHash !== validHash){
      res.status(403).json({ status: 'bad hash'}) // bad hash
    }
  }catch(err){
    res.status(500).json({ status: 'catch-all no'})
  }

  // otherwise, if everything checks out ..
  let responseOutput = { status: 'success' }
  if(req.body){
    // how to access data sent by client
    responseOutput['received_data'] = req.body
  }
  res.status(200).json(responseOutput)
}
vwatel commented 1 year ago

Any updates on this one? we are also looking for a solution to use the anti-CSRF implementation in next-auth for other API routes

OZZlE commented 1 year ago

Maybe like this? Btw why is this closed? https://stackoverflow.com/a/76110413/846348

ntaranov commented 1 year ago

@jayliew, @typedashutosh, thanks for the code!

@typedashutosh, please note that in

  for (let i = 0; i < rawCookiesArr.length; i++) {
    // loop through cookies to find CSRF from next-auth
    let cookieArr = rawCookiesArr[i].split('=')
    if (cookieArr[0].trim().search('next-auth.csrf-token')) {   
     // because on vercel, token is named _Host-next-auth.csrf-token
      parsedCsrfTokenAndHash = cookieArr[1]
      break
    }
  }

String search() returns truthy -1 for "not found" or position for "found". You probably intended to write something like

if (cookieArr[0].trim().search('next-auth.csrf-token') !== -1) {
swport commented 1 year ago

why isn't it a publicly exposed method instead of us rolling out our own solution which can easily break if we're not careful enough.

ricventu commented 10 months ago

I did this in Next 14, with app router thanks to @typedashutosh

import { createHash } from 'crypto'
import { cookies } from 'next/headers'

export const verifyCsrfToken = (): boolean => {
  try {
    const cookie = cookies().get('next-auth.csrf-token')
    if (!cookie) {
      return false
    }
    const parsedCsrfTokenAndHash = cookie.value

    if (!parsedCsrfTokenAndHash) {
      return false
    }
    // delimiter could be either a '|' or a '%7C'
    const tokenHashDelimiter =
          parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'

    const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(
      tokenHashDelimiter
    )

    const secret = process.env.NEXTAUTH_SECRET

    // compute the valid hash
    const validHash = createHash('sha256')
      .update(`${requestToken}${secret}`)
      .digest('hex')
    if (requestHash !== validHash) {
      return false
    }

  } catch (err) {
    return false
  }
  return true
}

in my API route:

  if (!verifyCsrfToken()) {
    return NextResponse.json({ error: 'Invalid CSRF token' }, { status: 403 })
  }