Next-Auth Credentials Provider with Laravel - Request failed with status code 419
Asked Answered
C

4

7

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.

Chronological answered 13/11, 2021 at 21:14 Comment(4)
Does this answer your question: Why are cookies not sent to the server via getServerSideProps in Next.js?? The question mentions getServerSideProps but the same applies to API routes, they both run on the server.Ireland
@Ireland 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?Chronological
@Chronological Did you find any solution for the same? I am facing same issueFaucal
@AmarUbhe Unfortunately I didn't find an exact solution but I stumbled on a repository by the Laravel team - github.com/laravel/breeze-next which gave some ideas. This video by them also helped - youtube.com/watch?v=Urgstu-mCecChronological
H
13

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

Hullabaloo answered 6/7, 2022 at 0:25 Comment(10)
I was using this solution on local, but when I deploy production, getting an error token mismatch :(Concordia
@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.Hullabaloo
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. :(Concordia
@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.Hullabaloo
hi @Jorge Roa, I will check this config. thank you very much for your time and insights. Appreciated it. Have a good day!Concordia
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]Woodchopper
@Woodchopper 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.Hullabaloo
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.Woodchopper
@Woodchopper 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. You can see and check this. If you arent using similar configuration, change your code and test again.Hullabaloo
My issue was not sending back the cookies in nextauth, everything's working now, thanks.Woodchopper
L
0

take an eye of these lines:

apiClient.get
apiClient.post

The baseURL is important, because if you are calling from a Server Component you need to make sure that you can reach the server. In my case, my problem was that I use 2 docker containers (1 for the frontend & 1 for the backend). I solved the problem using 2 axios client: an apiClient and a serverClient. The only difference is the baseURL. axios.tsx:

import axios, { AxiosHeaders } from 'axios';
import { API_USER_ACCESS_CONTROL_SERVER_URL } from './constants';

export const axiosClient = axios.create({
    baseURL: API_USER_ACCESS_CONTROL_SERVER_URL,
    withCredentials: true,
});

export const axiosServer = axios.create({
    baseURL: process.env.NEXTAUTH_URL_INTERNAL,
    withCredentials: true,
});

The NEXTAUTH_URL_INTERNAL (NEXTAUTH_URL_INTERNAL=http://auac-nginx) was the container_name of the nginx defined in the docker-compose.yml

finally in the authorize method:

await axiosServer.get('/sanctum/csrf-cookie');
const cookieStore = cookies();
const cookie = cookieStore.get('XSRF-TOKEN');
console.log('register...', cookie);

const response = await axiosServer.post('/api/v1/auth/authenticate', {
    email: credentials?.username,
    password: credentials?.password,
},
{
    headers: {
        'X-XSRF-TOKEN': cookie?.value,
    },
});
console.log(response);

where the cookie is from import { cookies } from 'next/headers'

Hope this help!

Lauzon answered 13/3 at 14:4 Comment(0)
C
-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
        })
}
Chabot answered 12/6, 2022 at 10:2 Comment(0)
E
-4

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'
Ealing answered 28/3, 2022 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.