6

I have a NextJS Frontend with Next-Auth installed and a Laravel Backend using Sanctum When I try to login using the signIn function of Next-Auth, it gives me this error:

Request failed with status code 419

419 has to do with CSRF token but I am setting the token by calling the sanctum/csrf-cookie route before calling the login method

[...nextauth.js]

CredentialsProvider
        ({
            name: 'Email and Password',
            credentials: {
                email: {label: "Email", type: "email", placeholder: "Your Email"},
                password: {label: "Password", type: "Password"}
            },
            async authorize({email, password}, req) {
                await apiClient.get("/sanctum/csrf-cookie");
                const user = await apiClient.post('/customer/login', {
                    email: email,
                    password: password,
                });

                if (user) {
                    return user
                } else {
                    return null

                }
            }
        })

apiClient.js

import axios from 'axios';

const apiClient = axios.create({
    baseURL: 'http://localhost:8000',
    withCredentials: true,
});

export default apiClient;

The first time I try to sign in, I get redirected to /api/auth/signIn?csrf=true and when I try to sign in again I'm redirected to /api/auth/error?error=Request failed with status code 419

I tried accessing the backend login routes using an API call from the client and it worked without any hitches.

Why is it failing for a request between two servers while it works fine when called from the client? I am not entirely grasping why the Next server isn't able to send a request with the csrf header to the Laravel Server. Is the cookie not set by the first call to sanctum/csrf-cookie when it comes to the server? Does CSRF not apply when talking between two server?

What am I missing here? Any help would be appreciated.

Following a comment, I tried explicitly passing passing the cookies following this question - Why are cookies not sent to the server via getServerSideProps in Next.js? but I still get a CSRF token mismatch error.

Arjun
  • 199
  • 2
  • 12
  • 1
    Does this answer your question: [Why are cookies not sent to the server via getServerSideProps in Next.js?](https://stackoverflow.com/a/69058105/1870780)? The question mentions `getServerSideProps` but the same applies to API routes, they both run on the server. – juliomalves Nov 14 '21 at 15:08
  • @juliomalves I tried that now but I still get a CSRF token mismatch error. Maybe it's because the browser CSRF token isn't really valid when sent from the server? – Arjun Nov 15 '21 at 14:53
  • 1
    @Arjun Did you find any solution for the same? I am facing same issue – Amar Ubhe Dec 26 '21 at 15:20
  • 1
    @AmarUbhe Unfortunately I didn't find an exact solution but I stumbled on a repository by the Laravel team - https://github.com/laravel/breeze-next which gave some ideas. This video by them also helped - https://www.youtube.com/watch?v=Urgstu-mCec – Arjun Dec 27 '21 at 17:25

3 Answers3

11

After a few days I found a solution for this which worked for me.

First, we need to understand that the code of [...nextauth.js] is server side, so this is run in Node.js, not in the browser, and so we need to set the cookies manually in all the requests we will do on the server side.

From the Laravel Sanctum Documentation:

If your JavaScript HTTP library does not set the value for you, you will need to manually set the X-XSRF-TOKEN header to match the value of the XSRF-TOKEN cookie that is set by this route

So we need to add the cookies manually to the request. Here's the code:

[...nexthauth.js]

import NextAuth from "next-auth"
import Credentials from 'next-auth/providers/credentials'
import axios from "../../../lib/axios";

//This is for getting the laravel-session cookie and the CSRF cookie 
//from any response of Sanctum or API Breeze
//In my case, the cookies returned are always two and I only need this, 
//so you can edit for get independent of position and cookies.
const getCookiesFromResponse = (res) => {
    let cookies = res.headers['set-cookie'][0].split(';')[0] + '; ' 
    cookies += res.headers['set-cookie'][1].split(';')[0] + '; '
    return cookies
}

//This is to get the X-XSRF-TOKEN from any response of Sanctum or API Breeze, 
//In my case, the token is always returned first, 
//so you can edit for get independent of position
const getXXsrfToken = (res) => {
    return decodeURIComponent(res.headers['set-cookie'][0].split(';')[0].replace('XSRF-TOKEN=',''))
}

//This method works to make any request to your Laravel API
//res_cookies are the cookies of the response of last request you do
//obviously res_cookies is null in your first request that is "/sanctum/csrf-cookie"
const makeRequest = async (method='get', url, dataForm = null, res_cookies ) => {
    const cookies = res_cookies != null ? getCookiesFromResponse(res_cookies) : null
    const res = await axios.request({
        method: method,
        url: url,
        data: dataForm,
        headers: {
            origin: process.env.NEXTAUTH_URL_INTERNAL, // this is your front-end URL, for example in local -> http://localhost:3000
            Cookie: cookies, // set cookie manually on server
            "X-XSRF-TOKEN": res_cookies ? getXXsrfToken(res_cookies) : null
        },
        withCredentials: true,
        credentials: true,
    })
    return res
}

const nextAuthOptions = (req, res) => {
    return {
        providers: [
            Credentials({
                name: 'Email and Password',
                credentials: {
                    email: { label: "Email", type: "email", placeholder: "Your Email" },
                    password: {  label: "Password", type: "password" }
                },
                async authorize(credentials) {
                    const csrf = await makeRequest('get', '/sanctum/csrf-cookie', null, null)
                    const user = await makeRequest('post', '/customer/login',  credentials, csrf )

                    if(user) return user
                    return null
                   
                }
            })
        ]
    }
}

lib/axios.js

import Axios from 'axios'

const axios = Axios.create({
    baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
    },
    withCredentials: true,
    credentials: true
})

export default axios

Now if you need to send the cookies to the front-side (browser), you can see this answer

Jorge Roa
  • 136
  • 1
  • 9
  • I was using this solution on local, but when I deploy production, getting an error token mismatch :( – Kalana Perera Aug 08 '22 at 02:16
  • @KalanaPerera are you sending the "origin" in the head of request ? This is need in production and if not sending may cause the error of token mismatch. – Jorge Roa Aug 08 '22 at 14:38
  • Yes I was using 'Origin' i couldn't able to find this error, I guess this is related to my infra. Example: when I connect Prod website to my local backend it works properly, only happens when I use the production backend. (AWS) I've deployed Laravel Application on AWS serverless. I assume some config is causing the problem. Note: since this is urgent I had to exclude CsrfToken validation for the login route. :( – Kalana Perera Aug 09 '22 at 13:51
  • 1
    @KalanaPerera maybe .env configuration ? In my back-end .env I have this important variables: `APP_URL=https://api.backenddomain.cl` `FRONTEND_URL=https://www.frontenddomain.cl` `SESSION_DOMAIN=.backenddomain.cl` without first two variables, I receive the error of token mismatch. Probably is a problem with the configuration of AWS or Laravel :/ I'm sorry I can't help you. – Jorge Roa Aug 10 '22 at 01:55
  • hi @Jorge Roa, I will check this config. thank you very much for your time and insights. Appreciated it. Have a good day! – Kalana Perera Aug 10 '22 at 08:51
  • How did you manage to remain logged in/make subsequent requests after the initial login? After using /login to Laravel Sanctum, I want to get the user details at /api/user, but get 401. Was something from the login response supposed to be set? Note this is all in the authorize code block of [...next-auth] – wanna_coder101 Oct 04 '22 at 03:43
  • 1
    @wanna_coder101 For all new request you do in the block [...next-auth], you need pass manually the last request cookies. If you are using my code, so for the next request you need call `const userDetails = await makeRequest('get','/api/user', null, user)` where the _user_ variable is the last result of call _makeRequest_ method. – Jorge Roa Oct 05 '22 at 17:23
  • What about in the client side? Would we get those same request cookies, set into as the header and submit another api call, ie to /api/user on the page? After logging in and getting the user details on the backend, I saved the headers (with the res cookies) into session. Then set the header on the client side api call to /api/user, but getting 401 error. – wanna_coder101 Oct 06 '22 at 02:47
  • 1
    @wanna_coder101 I had the same problem, but only in production and the problem was I miss a configuration in the .env of Laravel Breeze. I uploaded simple configurations files to [here](https://github.com/jroaes/next-auth-with-breeze). You can see and check this. If you arent using similar configuration, change your code and test again. – Jorge Roa Oct 07 '22 at 16:47
  • My issue was not sending back the cookies in nextauth, everything's working now, thanks. – wanna_coder101 Oct 08 '22 at 04:51
-1

It's a session based communication. You should get a CSRF Token first from the server.

const csrf = () => axios.get('/sanctum/csrf-cookie')

Then in login or register you have to get it like this before you try to login or register.

const login = async (email, pass) => {
    await csrf()
    axios
        .post('/login', {email: email, password: pass})
        .then(() => mutate())
        .catch(error => {
            if (error.response.status !== 422) throw error
        })
}
Maseed
  • 533
  • 5
  • 19
-3

I had this issue and solved it after spending some hours just by replacing this line:

import axios from 'axios';

by this line:

import axios from '@/lib/axios'
rahman j89
  • 59
  • 8