reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.7k stars 1.17k forks source link

Cookies are not set with fetchBaseQuery #2095

Closed KLagdani closed 2 years ago

KLagdani commented 2 years ago

Hello,

I have tried setting cookies using RTK Query's fetchBaseQuery with the option credentials: 'include', but it is not working. While doing the same request using fetch does work and sets the cookies like it should.

My server is running on: http://127.0.0.1:4000/api

I am using webpack, and I tried without any proxy and with these 2 proxies, but it won't work in any case:

        proxy: {
            '/api':{
                changeOrigin: true,
                cookieDomainRewrite: 'localhost',
                target: 'http://localhost:4000',
                onProxyReq: (proxyReq) => {
                    if (proxyReq.getHeader('origin')) {
                      proxyReq.setHeader('origin', 'http://localhost:4000')
                    }
                }
            }
        }
     proxy: {
            '/api': {
               target: {
                  host: "127.0.0.1",
                  protocol: 'http:',
                  port: 4000
               },
               withCredentials: true,
            }
        },

This is authApi.tsx:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { LoginValues } from '../../schema/login.interfaces';

export const authApi = createApi({
  reducerPath: 'authApi',
  baseQuery: fetchBaseQuery({
    baseUrl: "http://127.0.0.1:8080/api", // "http://127.0.0.1:4000/api" when using no proxy in webpack
    credentials: 'include', //Tried here
  }),
  endpoints: (builder) => ({
    login: builder.mutation<void, LoginValues>({
      query: (user) => ({
        url: '/auth/login',
        method: 'POST',
        body: user,
        // credentials: 'include', //And here
      }),
    }),
  }),
});

export const { useLoginMutation } = authApi;

Here is how I use it

export const LoginContainer: React.FC = () => {

  const [login, result] = useLoginMutation();

   const submit = async (data: LoginFormValues) => {
    // This calls the api and returns the right cookies in Set-Cookie header, but cookies are not set in the Application
    await login({
        email: 'hi@c.com',
         password: 'hellohello',
     });

    // But when I try it with a simple fetch like the following, it returns the right cookies and they are set in the Application
    // Tried with no proxy on webpack
    // const rawResponse = await fetch('http://localhost:4000/api/auth/login', {
    //   method: 'POST',
    //   headers: {
    //     Accept: 'application/json',
    //     'Content-Type': 'application/json',
    //   },
    //   body: JSON.stringify({
    //     email: 'hi@c.com',
    //     password: 'hellohello',
    //   }),
    //   credentials: 'include',
    // });

  };
  return (
    <>
      <Button onClick={submit}>Login</Button>
    </>
  );

}
export default LoginContainer;

Any idea how to set those cookies ?

phryneas commented 2 years ago

That will 100% call fetch with credentials: 'include' in both cases. Are you 100% sure that the cookies have been set for the host and path you are sending the request to here?

KLagdani commented 2 years ago

That will 100% call fetch with credentials: 'include' in both cases. Are you 100% sure that the cookies have been set for the host and path you are sending the request to here?

I am a 100% sure the cookies are set from the server, when I use the exact same host and path with a simple fetch query, the cookies are set correctly on the Application.

phryneas commented 2 years ago

Hmm. Then all I could think of would be that you set a breakpoint in fetchBaseQuery and step through there to see what fetch is being called with :/ From here I can only say that yes, this should call fetch with credentials: 'include'

KLagdani commented 2 years ago

Thank you @phryneas , I'll try that. Would you say it is better to keep credentials: 'include' in baseQuery or per query in each endpoint?

phryneas commented 2 years ago

Depends if you want to send those cookies with every request. Usually I would assume you want that, so I'd add it to the baseQuery.

AlbinCederblad commented 2 years ago

I had the same issue. Turned out that chrome was filtering out the cookies from the request because it was a "cross-site" request and the "secure"-flag on the Cookie was set to false.

So after setting sameSite: "None", secure: true it's now working.

Elendil00 commented 2 years ago

Hi,

i'm facing the same issue: frontend on localhost:3000 and backend on localhost:8080 (CORS configuration ok).

Session cookie is not set in HTTP Request Headers when using RTK Query but no problem using Fetch api :/

I tried setting credentials: 'include' when creating Api

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
    reducerPath: 'api',
    baseQuery: fetchBaseQuery({ baseUrl: process.env.REACT_APP_API_SERVER_URL }),
    credentials: 'include',
    prepareHeaders(headers) {
        return headers;
    },
    endpoints: () => ({})
})

and when injecting endpoints

import {apiSlice} from 'api/apiSlice'

export const contentExtendedApiSlice = apiSlice.injectEndpoints({
    endpoints: (builder) => ({
       ...
        // Mutations
        addPost: builder.mutation({
            query: ({pageId, payload}) => {
                return {
                    url: `/pages/${pageId}/posts`,
                    method: 'POST',
                    credentials: 'include',
                    body: payload
                };
            }
        }),
    }),
})
export const {...} = contentExtendedApiSlice

But failed to have it working as expected like when using fetch api

fetch(' http://localhost:8080/api/pages/819008814840807426/posts', {
     method: 'POST',
     body: payload,
     credentials: 'include',
 })

@KLagdani did you solve the issue on your side ? maybe a missing configuration ?

Thanks

phryneas commented 2 years ago

That will execute exactly that fetch call in the background, assuming that the baseUrl is the same.

Elendil00 commented 2 years ago

@phryneas ha yes indeed, the issue was located between the keyboard and the chair :/

Hazem-Ben-Abdelhafidh commented 2 years ago

so what's the solution to this? I'm facing the same problem req.cookies always return that ugly [Object: null prototype] {} ...

CJ-0221 commented 2 years ago

I had the same issue. Turned out that chrome was filtering out the cookies from the request because it was a "cross-site" request and the "secure"-flag on the Cookie was set to false.

So after setting sameSite: "None", secure: true it's now working.

I tried doing same but it's ignoring. Could you pls share how you did it. Btw I'm using fetch. Pls suggest

kachmul2004 commented 1 year ago

@Elendil00 Hi, how did you resolve this?

arndvs commented 1 year ago

Hello @Elendil00, any updates on how to resolve this? Thanks!

iamAravindks commented 1 year ago

@arndvs @KLagdani I'm able to solve the issue, The only thing I did is the addition of a proxy to the application.

setupProxy.js

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    "/api",
    createProxyMiddleware({
      target: "http://localhost:5000",
      changeOrigin: true,
    })
  );
};

and change the baseQuery

  baseQuery: fetchBaseQuery({
    baseUrl: "api",
    headers: {
      "Content-Type": "application/json",
    },
    credentials:"include"
  }),
VektorTech commented 1 year ago

I encountered this same issue after deploying my MERN app to render.com. The problem isn't fetchBasedQuery but the cookies themselves. From this previous reply, When using cross-site cookies in chromium-based browsers, you have to set SameSite: "None" and Secure: true.

Also Remember to set the Access-Control-Allow-Credentials header to true, and if you're using express js behind a reverse proxy, add app.set('trust proxy', 1); as well.

WichoGZF commented 1 year ago

If both your applications are on the localhost check that your baseUrl is "127.0.0.1" and not "localhost" (or vice versa I suppose). For me that was what was causing the queries to not include the cookie.

herberthk commented 1 year ago

I had this issue, and it took me several hours to resolve and these were my findings.

  1. This issue is not related to redux-toolkit or fetchBaseQuery,
  2. You need to add credentials:"include" in your base query for example
    baseQuery: fetchBaseQuery({
    baseUrl: "api",
    credentials:"include"
    }),

    and that's all on the frontend side.

  3. If you manage a server make sure you enable credentials and setup origin in your CORS configuration for example with express.
    app.use(cors({
    credentials: true,
    origin: 'http://localhost:3000',
    }));

    Without setting them you'll get CORS errors.

  4. If you're using cookieSession remember to set signed to false, secure to false while running locally
capo33 commented 1 year ago

Hello! I had the same issue and i came with the solution just like so

1- on server side

app.use(cors(
    {
      origin: "http://localhost:3000/",
      credentials: true,
     }
));
res.cookie("jwt", token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict', // Enforce secure cookies & // Prevent CSRF attacks by setting sameSite
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});

2- in client side

a- apiSlice.js

export const apiSlice = createApi({
      baseQuery: fetchBaseQuery({
      baseUrl: BASE_URL,
      prepareHeaders: (headers) => {
      return headers;
    },
  }),
    tagTypes: ["Products", "Orders", "Users"], 
    endpoints: (builder) => ({}),
});

b- usersApiSlice.js

export const usersApiSlice = apiSlice.injectEndpoints({
      endpoints: (builder) => ({
      registerUser: builder.mutation({
        query: (userData) => ({
        url: ${USER_URL}/register,
        method: "POST",
        body: userData,
      }),
     }),
    loginUser: builder.mutation({
       query: (userData) => ({
       url: ${USER_URL}/login,
       method: "POST",
       body: userData,
       credentials: "include",
     }),
    }),
    logoutUser: builder.mutation({
      query: () => ({ 
      url: ${USER_URL}/logout,
      method: "POST",
      credentials: "include",
     }),
   }),
 }),
}),

I hope that help 😊

webkawsar commented 1 year ago

I had the same issue. Turned out that chrome was filtering out the cookies from the request because it was a "cross-site" request and the "secure"-flag on the Cookie was set to false.

So after setting sameSite: "None", secure: true it's now working.

this way solved my problem

morelattes commented 1 year ago

I am getting a Cannot POST /api/sessions

My baseQuery:

const baseQuery = fetchBaseQuery({
    baseUrl: "api",
    credentials: "include",    
    prepareHeaders: (headers, {getState}) => {
        const token = getState().auth.token
        if (token) {
            headers.set('authorization', `Bearer ${token}`)
        }       
        return headers
    }   
})

My proxy:

module.exports = function addProxyMiddleware(app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'https://<baseurl>:8081/',
      changeOrigin: true,
      secure: false
    })
  );
};

My endpoint:

    endpoints: builder => ({
        login: builder.mutation({
            query: credentials => ({
                url: '/sessions',
                method: 'POST',
                body: `{ "UserName": "${credentials.UserName}", "Password": "${credentials.Password}"}`,
                headers: {
                    "Content-type": "application/json; charset=utf-8",}
            })
        }),

The error that I'm getting is:

{
    "status": "PARSING_ERROR",
    "originalStatus": 404,
    "data": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST /api/sessions</pre>\n</body>\n</html>\n",
    "error": "SyntaxError: Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON"
}

However, I am able to POST using Postman. What am I missing here?

phryneas commented 1 year ago

@morelattes You are getting a HTML response and it tries to parse it as JSON and errors.

morelattes commented 1 year ago

@phryneas but the error is a "Cannot POST", which means that my baseurl has some problem right? Or maybe somewhere between the proxy and the RTK query I have written the wrong url?

phryneas commented 1 year ago

That's a problem with your server config then, not on the RTK Query side. As long as the outgoing request in your devtools looks right you have to look somewhere else.

MattHammond94 commented 9 months ago

Hello people!

I am facing this similar issue now that I have deployed my application using render and was hoping someone could help me.

I deployed my backend express server on render and now I have an issue whereby cookies are not being sent in REQ's made to my backend from my also deployed client side app.

In my client side app using Redux:

apiSlice:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const baseQuery = fetchBaseQuery({ 
  baseUrl: 'https://java-gram-backend.onrender.com/api',
  prepareHeaders: (headers) => {
    headers.set('credentials', 'include');
    return headers;
  },
});

export const apiSlice = createApi({
  baseQuery,
  tagTypes: ['User', 'Post'],
  endpoints: (builder) => ({})
});

Backend server:

generateToken:

import jwt from 'jsonwebtoken';

const generateToken = (res, userId) => {

  console.log(`NODE_ENV: ${process.env.NODE_ENV}`);  // Returns production

  const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
    // expiresIn: '1h'
  });

  res.cookie('jwt', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV !== 'development',
    secure: true,
    sameSite: 'None',
    // maxAge:
  });
}

export default generateToken;

app.js:

const corsOptions = {
  origin: 'https://java-gram-frontend.onrender.com',
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  credentials: true,
  optionsSuccessStatus: 204,
};

const app = express();

app.use(cors(corsOptions));

authMiddleware.js:

const protect = asyncHandler(async (req, res, next) => {
  let token;

  console.log(req.cookies);  

  token = req.cookies.jwt;

  console.log(token);  // Returns Undefined

  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      req.user = await User.findById(decoded.userId).select('-password');
      next();
    } catch(error) {
      res.status(401);
      throw new Error('')
    }
  } else {
    res.status(401);
    throw new Error('Unauthorized without a token')
  }
});

export { protect };

The endpoints which don't use the authMiddleware (signUp/LogIn) work as expected and successfully generate the token however the token is not stored and sent with subsequent requests thus returning a 401 from all protected endpoints.

I've tried a few different variations to get cookies to work in this scenario and am unsure how to get this to work.

Any help would be greatly appreciated!

phryneas commented 8 months ago

@MattHammond94 it could be necessary to set credentials: "include" in your fetchBaseQuery config, but apart from that that is really a problem between your browser and your server and doesn't really touch RTK Query. I'd suggest you debug that with a normal fetch call and leave RTKQ out of the picture until you solved it.

MattHammond94 commented 8 months ago

@phryneas Thanks for your advice! I will try adding credentials and failing that I will add a standard fetch call in an attempt to debug.

Pre-deployment when I was working in the dev env locally I did not face this issue. Do you have any theory as to why this is happening now and does the introduction of HTTPS have any effect I am unaware of?

FIDELIS-TUWEI commented 8 months ago

I have tried all the above mentioned examples and nothing seems to work. I tried deploying to aws ec2 and faced the same errors and thought maybe I was configuring nginx the wrong way, I then opted for render.com only for it to work for a few hours and started giving errors of not authorised yet the cookies have been set on the browser.

Here is my server file:

const dotenv = require("dotenv");
const path = require("path");
const connectDB = require("../config/connectDB");
const express = require('express');
let app = express();

connectDB();

// Server Setup
const PORT = process.env.PORT || 5500;

// Middleware
require("./middleware")(app);

//Routes
require("./routes/index")(app);

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`)
});

// Create route
app.get("/", (req, res) => {
    res.send("Server is running");
})

process.on('unhandledRejection', error => {
    console.error('unhandledRejection', error)
});

index.js file: 
module.exports = (app) => {
    const express = require('express');
    const cors = require('cors');
    const rateLimit = require("express-rate-limit");
    const bodyParser = require('body-parser');
    const helmet = require("helmet");
    const morgan = require("morgan");
    const cookieParser = require("cookie-parser");
    const errorHandler = require("../middleware/error");
    const mongoSanitize = require("express-mongo-sanitize");
    const hpp = require("hpp");
    const addKey = require("./addKey");

    // Express trust proxy settings
    app.set('trust proxy', 1)
    app.get('/ip', (request, response) => response.send(request.ip))
    app.get('/x-forwarded-for', (request, response) => response.send(request.headers['x-forwarded-for']));
    app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']) // specify multiple subnets as an array

    // Add Key Generator Middleware
    app.use(addKey);

    // Middleware
    let limiter = rateLimit({
        max: 200,
        windowMs: 10 * 60 * 1000,
        message: "Too many requests from this IP. Please try again later."
    });

    app.use('/hin', limiter);
    app.use(helmet());
    app.use(helmet.crossOriginResourcePolicy({ policy: "same-origin" }));

    if (process.env.NODE_ENV !== 'production') {
        app.use(morgan('dev'));
    }
    app.use(bodyParser.json({ limit: "5mb" }));
    app.use(bodyParser.urlencoded({ 
        limit: "5mb",
        extended: true 
    }));
    app.use(cors({
        credentials: true,
        origin: ["http://localhost:3000", "https://www.work-orders.online", "https://work-orders.online"],
        allowedHeaders: ["Authorization", "Content-Type"],
        maxAge: 3600,
        methods: ["GET", "POST", "PUT", "DELETE"],
    }));
    app.use(cookieParser());
    app.use(express.json()); // To parse JSON data in the request body
    app.use(express.urlencoded({ extended: true })); // To parse form data in the request body
    // Prevent HTTP Parameter pollution
    app.use(hpp());

    // Prevent XSS
    app.use(helmet.contentSecurityPolicy());

    // Prevent SQL Injection
    app.use(mongoSanitize());

    // Error Middleware
    app.use(errorHandler);
};

protect middleware: 

const protect = asyncHandler(async (req, res, next) => {
    try {
        const token = req.cookies.token;
        if (!token) {
            return next(new ErrorResponse("Not authorized, invalid token", 401));
        }

        // Verify token
        const verified = jwt.verify(token, process.env.JWT_SECRET);

        // Get user id from the token
        const user = await User.findById(verified.id);

        if (!user) {
            return next(new ErrorResponse("User not found, please login", 401));
        }
        req.user = user;

        next();
    } catch (error) {
        return next(new ErrorResponse("Not authorized, no token", 401));
    }

});

Redux rtk configuration files:

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

const baseQuery = fetchBaseQuery({ 
    baseUrl: "",
    credentials: "include",
    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token;
        if (token) {
            headers.set("authorization", `Bearer ${token}`);
        }
        return headers;
    } 
});

export const apiSlice = createApi({
    baseQuery,
    tagTypes: ["Users", "WorkOrder"],
    endpoints: (builder) => ({}),
});

import { apiSlice } from "../api/apiSlice";
const serverUrl = import.meta.env.VITE_SERVER_URL

export const authApiSlice = apiSlice.injectEndpoints({
    endpoints: (builder) => ({
        login: builder.mutation({
            query: (data) => ({
                url: `${ serverUrl }/login`,
                method: "POST",
                body: data,
            }),
        }),
        logout: builder.mutation({
            query: () => ({
                url: `${ serverUrl }/logout`,
                method: "POST",
            }),
        }),
    })
});

export const { 
    useLoginMutation, 
    useLogoutMutation, 
} = authApiSlice;

rtk to get employees:

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
const serverUrl = import.meta.env.VITE_SERVER_URL;

const baseQuery = fetchBaseQuery({
    baseUrl: serverUrl,
    credentials: "include",
    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token;
        if (token) {
            headers.set("Authorization", `Bearer ${token}`);
        }
        return headers;
    },
});

export const employeesApi = createApi({
    reducerPath: "employeesApi",
    baseQuery,
    tagTypes: ["Employee"],
    endpoints: (builder) => ({
        createEmployee: builder.mutation({
            query: (values) => ({
                url: `/new/employee`,
                method: "POST",
                body: values,
            }),
            invalidatesTags: ["Employee"],
        }),
        employees: builder.query({
            query: (page) => ({
                url: `/all/employees?pageNumber=${page}`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        singleEmployee: builder.query({
            query: (id) => ({
                url: `/single/employee/${id}`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        employeeData: builder.query({
            query: (id) => ({
                url: `/employee/data/${id}`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        queryAllEmployees: builder.query({
            query: () => ({
                url: `/query/all/employees`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        employeeWorkCount: builder.query({
            query: (id) => ({
                url: `/employee/work/count?id=${id}`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        countAllEmployees: builder.query({
            query: () => ({
                url: `/count/employees`,
                method: "GET",
            }),
            providesTags: ["Employee"],
        }),
        editEmployee: builder.mutation({
            query: ({id, values}) => ({
                url: `/edit/employee/${id}`,
                method: "PUT",
                body: values,
            }),
            invalidatesTags: ["Employee"],
        }),
        deleteEmployee: builder.mutation({
            query: (id) => ({
                url: `/delete/employee/${id}`,
                method: "DELETE",
            }),
            invalidatesTags: ["Employee"],
        }),
    }),
});

export const {
    useCreateEmployeeMutation,
    useEmployeesQuery,
    useSingleEmployeeQuery,
    useEmployeeDataQuery,
    useQueryAllEmployeesQuery,
    useEmployeeWorkCountQuery,
    useCountAllEmployeesQuery,
    useEditEmployeeMutation,
    useDeleteEmployeeMutation,
} = employeesApi;

Can anyone help, please

Yeiba commented 7 months ago

it work for me

`const baseQuery = fetchBaseQuery({

baseUrl: ${process.env.NEXT_PUBLIC_HOST}/api, credentials: "include", mode: "cors", headers: { "Content-Type": "application/json", }, prepareHeaders: (headers) => { return headers; }, });`

arbnormiftari99 commented 6 months ago

Hello! I had the same issue and i came with the solution just like so

1- on server side

app.use(cors(
    {
      origin: "http://localhost:3000/",
      credentials: true,
     }
));
res.cookie("jwt", token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict', // Enforce secure cookies & // Prevent CSRF attacks by setting sameSite
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});

2- in client side

a- apiSlice.js

export const apiSlice = createApi({
      baseQuery: fetchBaseQuery({
      baseUrl: BASE_URL,
      prepareHeaders: (headers) => {
      return headers;
    },
  }),
    tagTypes: ["Products", "Orders", "Users"], 
    endpoints: (builder) => ({}),
});

b- usersApiSlice.js

export const usersApiSlice = apiSlice.injectEndpoints({
      endpoints: (builder) => ({
      registerUser: builder.mutation({
        query: (userData) => ({
        url: ${USER_URL}/register,
        method: "POST",
        body: userData,
      }),
     }),
    loginUser: builder.mutation({
       query: (userData) => ({
       url: ${USER_URL}/login,
       method: "POST",
       body: userData,
       credentials: "include",
     }),
    }),
    logoutUser: builder.mutation({
      query: () => ({ 
      url: ${USER_URL}/logout,
      method: "POST",
      credentials: "include",
     }),
   }),
 }),
}),

I hope that help 😊

This stupid thing took me 1 day till i saw your post man! I tried credential: "true" and a lot of others things and the yours one " credential: "include" " worked! Many thanks!

YenXXXW commented 4 months ago

https://github.com/reduxjs/redux-toolkit/issues/2095#issuecomment-1569869298 Thanks this helped