PHP Session to React without CORS issue (or: how to solve PHP Session is changing on every request). Bonus: CSRF!

If you are developing a React app with a PHP backend (or a PHP framework such as Laravel) maybe you have encounter this issue where:

  • The php session id is changed on every request
  • When you save data into the session on the backend, it is not available for you on the next request.

The issues above are actually related. Since the session id changes on every new request, the data is saved into a new session. Subsequent request don’t use the previously generated session, so they can’t get the data that was saved into it.

This is happening especially during development, because using create_react_app is deploying the local app on http://localhost:3000 while your backend might be on another domain.

For me, I was using docker as a development server, and I connected to it using internal IP (172.17.0.2, for example). As a result, the communication between the frontend and the server backend becomes cross domains, which complicates the things a little bit.

The Solution

If you just came here for the solution, then add following changes to your code. Below I will explain some stuff that I have observed about how it works, so you can check if you need to debug.

Add this line to App.js:

axios.defaults.withCredentials = true;

I am using axios, if you are using something else, try to find what you need to add. Basically what this means is that axios will try to add the session cookie in the request.

In the backend, add the following code:

ini_set("session.cookie_domain", '.dev.local');
session_set_cookie_params(3600, '/', '.dev.local');
if(!isset($_SESSION)) {
session_start();
}
// csrf code add here (see below...)$http_origin = $_SERVER['HTTP_ORIGIN'];
if ($http_origin == "http://dev.local:3000" || $http_origin == "http://localhost:3000"){
header("Access-Control-Allow-Origin: $http_origin");
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Headers: X-Requested-With, Origin, Content-Type, X-CSRF-Token, Accept');
// code starts here
$_SESSION['test'] = 'whatever';
session_write_close();

Ideally session stuff should be the first thing that the php backend code does when the page loads. Make sure that no output is send before session_write_close() is called, otherwise you will get an error that headers where are already sent.

Notice that I am using the domain dev.local. You need to add it to your hosts file.

The last thing is that you need to use the domain in order to access the app. So instead of using http://localhost:3000, use http://dev.local:3000.

It should work…

Below I will add some remarks of what I have observed of how it is working (I think). If things are not working, you can try to compare, maybe it can help you debug.

How it works

When you open devtools, and you looks at the requests, what we want to see is that requests are going out with a cookie named PHPHSESSID that has the same value as the Set-Cookie directive that was included in the response header.

If you don’t have this PHPSESSID, or it has a different value on every request, then something is wrong.

The value of PHPSESSID should be the same on all POST and GET requests (and responses).

PHP is generating a session id and sends it in the first response to the frontend. It expects to see the same session id in the request, otherwise it will start a new session

This is how the response headers of the first communication looks like:

See the PHPSESSID in #1. Also notice the Access-Control-Allow-* directives (#2) in the response, which are the same as we set them in the backend (more on that below).

Subsequent requests from the client, all have the (same) PHPSESSID cookie in their request headers:

I have noticed that in preflight requests, (request that are of type Request Method: OPTIONS) , the value of PHPSESSID is different. As far as I have seen, this is not a problem.

If you are wondering what are these preflight messages, then this is where the browser is asking the server how to send the data. This is where the headers that defined above in the server side should take affect.

The frontend will add the PHPSESSID cookie to the response only if:

  1. The Access-Control-Allow-* methods from the server allow it.
  2. It has to be on the same domain as the backend.
  3. For some reason, it didn’t work with http://localhost . Another reason why I needed to use the http://dev.local domain.

For #2, the port is not considered as a different domain. So the cookie was added since frontend is running on http://dev.local:3000 while backend on http://dev.local .

If you omit or change some of the Access-Control-Allow-* directives, you will encounter the dreadful CORS errors, that I am sure you have seen in the past.

Here are some examples:

The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’.

Now that you understand how the code works, it is quite self explanatory. Since the client (React), is using the withCredetials directive, the backend should have the Access-Control-Allow-Credential set the true.

The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’

Again, very simple. Since we are using Access-Control-Allow-Credential, we must need to provide one domain (and one domain only) in the Access-Control-Allow-Origin directive.

You get the picture, I hope..

What about CSRF?

When using sessions, you need to protect again CSRF. CORS by itself doesn’t protect against these types of attacks, so we will need to add additional code.

In the backend, add this:

$token_mismatch = isset($_SESSION['token']) && $_SESSION['token']!=$_SERVER['HTTP_X_CSRF_TOKEN'];
$token_request = isset($_GET['method']) && $_GET['method'] != "getCSRFToken");
if ( $token_mismatch && !$token_request) {
header("HTTP/1.1 401 Unauthorized");
exit;
}
if (!isset($_SESSION['token'])){
$token = md5(uniqid(mt_rand(), true));
$_SESSION['token'] = $token;
}
header("token: $token");
if ($_GET['method'] == "getCSRFToken"){
echo $_SESSION['token'];
exit;
}

This code is in charge of:

  • preventing access if token is present but mismatched
  • If session doesn’t have a csrf token, generate a random token, store it in the session, and add it to the response.
  • Add an endpoint for the frontend to inquire the csrf token.

In your frontend, add a file csrf.js :

import axios from "axios";export default function getCSRFToken() {
return new Promise((resolve, reject) => {
if (window.csrf_token_status === "available") return resolve();
if (window.csrf_token_status === "requested") {
setInterval(function(){
if (window.csrf_token_status === "available")
return resolve();
}, 500);
}else{
const server_url = window.ENV.site_url;
window.csrf_token_status = "requested";

axios
.get(server_url+'?method=getCSRFToken')
.then((response)=>{
axios.defaults.headers.post['X-CSRF-Token'] = response.data;
window.csrf_token_status = "available";
return resolve();
})
}
});
};

Then add these lines to App.js:

import getCSRFToken from './csrf';
window.csrf_token_status = "unavailable";
const App = () => {
useEffect(() => {
getCSRFToken();
}, [])
}

If users starts a session, but then refreshes the tab, it will make a new request to get the csrf token.
Frontend then adds adds the token to every request using axios.defaults
There is also a bunch of code that is used to make sure there is only one csrf get token request, and waiting until a token is available.

The End

And I hope that this post will help you with your session issue. If it did help you, or you have questions, or you have a improvement suggestion, do comment or reach out, I am always happy to know/assist.

--

--

--

Full Stack, Data Science, ML AI

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

//platform.twitter.com/widgets.js from Twitter https://twitter.com/UKCareGuide

How to Test a JavaScript Application Like a Pro? With What Tools? (Part 2)

Mobile friendly navigation in React and HOC

JavaScript Algorithm: Letter Changes

Ivy: A look at the New Render Engine for Angular

JUnit: Testing of log output

4 ways to remove duplicates from an array in JavaScript

❼❹❼❽❷❾❾❾❶❽Agile loan.Customer.Care.number❼❹❼❽❷❾❾❾❶❽Agile loan.Customer.Care.number

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
hezi hershkovitz

hezi hershkovitz

Full Stack, Data Science, ML AI

More from Medium

Make iTerm and JetBrains IDEs work together

Create a script to compress folders in ZIP with PHP

How to setup a Svelte with Typescript project

WHY USE TYPESCRIPT?