Welcome to a tutorial on how to support cross-domain sessions in PHP. Yes, it is technically possible to share a single session across multiple domains, but it is quite a challenging task.
To support cross-domain sessions in PHP, we need to:
- Save the sessions into a shared common database.
- Set one of your websites to facilitate the shared session.
That covers the basic idea, read on for the details!
ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.
TABLE OF CONTENTS
DOWNLOAD & NOTES
Firstly, here is the download link to the example code as promised.
QUICK NOTES
- This is an advanced topic. It is best that you have a solid understanding of cross origins, cookies, and sessions in PHP before proceeding.
- This is based on Apache Web Server, although we can also easily set up virtual hosts on any other HTTP servers.
EXAMPLE CODE DOWNLOAD
Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
TEST SERVER SETUP
Before we get into the code, here is a quick walkthrough and overview of the server setup – If you don’t have multiple domains for testing, no sweat. Here is how we can simulate multiple domains in localhost.
SETUP OVERVIEW
For this example, we are going to have 2 websites –
site-a.com
and site-b.com
. Both websites have access to a shared database.
OS HOSTS
127.0.0.1 site-a.com
127.0.0.1 site-b.com
::1 site-a.com
::1 site-b.com
First, open the hosts
file and add in the above. For the uninitiated, don’t panic. This is just a manual DNS override to point site-a.com
and site-b.com
to your local server.
P.S. Linux and Mac users, your host file should be located in /etc/hosts
.
VIRTUAL HOSTS
<VirtualHost *:80 *:443>
DocumentRoot "D:/http/"
ServerName localhost
</VirtualHost>
<VirtualHost *:80 *:443>
DocumentRoot "D:/http/site-a/"
ServerName site-a.com
</VirtualHost>
<VirtualHost *:80 *:443>
DocumentRoot "D:/http/site-b/"
ServerName site-b.com
</VirtualHost>
Next, we add the virtual hosts to “map” https://site-a.com
to YOUR-PATH/site-a/
and https://site-b.com
to YOUR-PATH/site-b/
.
P.S. This is for the Apache webserver. IIS and NGINX users, please do your own research.
SSL CERTIFICATE
Lastly, using https://
is a huge part to make this entire example work. If you have not already set this up, self-sign and set your own SSL cert. It is quite a hassle to do it, but there are many good guides online – Do your own research.
STEP 1) DATABASE SESSIONS TABLE
CREATE TABLE `sessions` (
`id` varchar(32) NOT NULL,
`access` int(10) UNSIGNED DEFAULT NULL,
`data` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `sessions`
ADD PRIMARY KEY (`id`);
Yes, PHP sessions are file-based by default. That will not work for a distributed setup, so the first step is to create a shared database to store the sessions instead. This should be pretty self-explanatory:
id
The session ID.access
Last accessed timestamp.data
Session data.
STEP 2) DATABASE SESSIONS CLASS
<?php
// (A) DATABASE SESSION CLASS
class Session {
// (A1) PROPERTIES
public $pdo = null;
public $stmt = null;
public $error = "";
public $lastID = null;
// (A2) INIT - DATABASE SESSION
function __construct () {
session_set_save_handler(
[$this, "open"], [$this, "close"], [$this, "read"],
[$this, "write"], [$this, "destroy"], [$this, "gc"]
);
session_start();
}
// (A3) EXECUTE SQL QUERY
function exec ($sql, $data=null) {
try {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
$this->lastID = $this->pdo->lastInsertId();
return true;
} catch (Exception $ex) {
$this->error = $ex->getMessage();
return false;
}
}
// (A4) SQL FETCH
function fetch ($sql, $data=null) {
if (!$this->exec($sql, $data)) { return false; }
return $this->stmt->fetch();
}
// (A5) SESSION OPEN - CONNECT TO DATABASE
function open ($savePath, $sessionName) {
try {
$this->pdo = new PDO(
"mysql:host=".SDB_HOST.";charset=".SDB_CHAR.";dbname=".SDB_NAME,
SDB_USER, SDB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
return true;
} catch (Exception $ex) { exit ($ex->getMessage()); }
}
// (A6) SESSION CLOSE - CLOSE DATABASE
function close () {
if ($this->stmt !== null) { $this->stmt = null; }
if ($this->pdo !== null) { $this->pdo = null; }
return true;
}
// (A7) SESSION READ
function read ($id) {
$result = $this->fetch("SELECT * FROM `sessions` WHERE `id`=?", [$id]);
return is_array($result) ? $result['data'] : "" ;
}
// (A8) SESSION WRITE
function write ($id, $data) {
return $this->exec(
"REPLACE INTO `sessions` VALUES (?, ?, ?)",
[$id, time(), $data]
);
}
// (A9) SESSION DESTROY
function destroy ($id) {
return $this->exec("DELETE FROM `sessions` WHERE `id`=?", [$id]);
}
// (A10) GARBAGE COLLECTOR
function gc ($max) {
return $this->exec("DELETE FROM `sessions` WHERE `access` < ?", [(time() - $max)]);
}
}
// (B) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("SDB_HOST", "localhost");
define("SDB_CHAR", "utf8");
define("SDB_NAME", "test");
define("SDB_USER", "root");
define("SDB_PASS", "");
// (C) START!
$SDB = new Session();
Not going to explain this library line-by-line, but we basically use session_set_save_handler()
to set our own custom PDO database session handlers. Remember to change the database settings (B) to your own.
STEP 3) ACCESS SITE B
Before we proceed with the next step, open a new tab in your browser and access
https://site-b.com
. Yes, that is HTTPS
. If you have self-signed your own SSL cert, your browser or anti-virus is going to complain “not secure”. No s*it Sherlock, I signed the certificate myself. So, just click on “continue”.
STEP 4) SITE A – SESSION STARTER & SYNC
<!-- (A) HTML + START PHP SESSION -->
<div><?php
require "../2-lib-sess.php";
$_SESSION = ["Start" => date("Y-m-d H:i:s")];
echo session_id();
print_r($_SESSION);
?></div>
<script>
// (B) PHP SESSION ID
var sid = new FormData();
sid.append("sid", "<?=session_id()?>");
// (C) START SYNC
window.addEventListener("load", () => {
// (C1) SITES TO SYNC
var sites = ["site-b.com"];
// (C2) FETCH SYNC REQUEST
for (let site of sites) {
// CALL SYNC HANDLER
fetch(`https://${site}/4-sync.php`, {
mode : "cors",
method : "post",
credentials : "include",
body : sid
})
// LOG ALL RESPONSE FOR DEBUG
.then((res) => {
console.log(res);
return res.text();
})
.then((txt) => { console.log(txt); })
.catch((err) => { console.error(err); });
}
});
</script>
- (A) Remember the database session library we create earlier on? Loading this library will automatically start the session, and we set a timestamp into the session for testing.
- (B & C) Getting the rest of the domains to sync is tricky. We have to do a cross-origin AJAX fetch call with the session ID to set the cookie.
STEP 5) SITE B – SESSION SYNC
<?php
header("Access-Control-Allow-Origin: https://site-a.com");
header("Access-Control-Allow-Credentials: true");
setcookie("PHPSESSID", $_POST["sid"], [
"path" => "/",
"domain" => "site-b.com",
"secure" => true,
"samesite" => "None"
]);
echo "OK";
This is the “session sync handler” on site-b.com
. Basically, just setting the PHPSESSID
cookie. Yep, if you don’t know what that is, roll back and follow up with your own studies on the basics. Links below.
STEP 6) ACCESS SITE A
Go ahead and access https://site-a.com/3-index.php
(if you have not already done so). Open up the developer’s console > Application > Cookies. Both site-a.com
and site-b.com
should have the same PHPSESSID
.
STEP 7) VERIFY SITE B
<?php
require "../2-lib-sess.php";
print_r($_SESSION);
Then, access https://site-b.com/5-verify.php
– Congratulations, the sessions have synced; Both domains now share a single session in the database.
USEFUL BITS & LINKS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
SECURITY CHECKS
Right, the above “fetch and sync” cycle is not secure at all. You will want to beef up your own for better security. For example:
- Create a “sync request” table with 2 fields –
hash
andexpiry
. - On accessing
https://site-a.com
, create a randomhash
and set theexpiry
to 1 minute from now. - The fetch call should also include the random
hash
. - The PHP sync will cross-check the
hash
against the database to verify and make sure it has not expired.
Basically, a CSRF token.
I WANT TO “SYNCHRONIZE” MORE SITES
- Just deploy the same PHP sync handler on all your other domains.
- Add your other sites into the fetch call.
COMPATIBILITY CHECKS
- Same Site Headers & Cookies – CanIUse
- CORS – CanIUse
- Arrow Functions – CanIUse
- Fetch – CanIUse
This example requires a “Grade A” modern browser that is capable of handling cross-origin calls and cookies.
LINKS & REFERENCES
- Cookies In PHP – Code Boxx
- Sessions In PHP – Code Boxx
- Setting CORS Cookie In PHP – Code Boxx
- Edit Windows, Mac, Linux Host File – HowToGeek
- Virtual Hosts Example – Apache
- Set Session Handler – PHP
THE END
Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!
Unfortunately this doesn’t work on Safari browsers
Hi! Please see the above “compatibility checks” and also share more details on “doesn’t work”. “Doesn’t work” can be “anything”, “whatever”, and “whatnot”… It “doesn’t help”. 😆
https://code-boxx.com/faq/#notwork