Cross Domain Session In PHP (Step-by-Step Example)

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 Server Setup Shared Session
Useful Bits & Links The End

 

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 cookies and sessions in PHP before proceeding.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

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

C:\Windows\System32\drivers\etc\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

httpd.conf
<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.

 

PHP SHARED SESSION

Now that the server setup is in place, let us walk through the steps to support cross-domain sessions.

 

STEP 1) DATABASE SESSIONS TABLE

1-sess-db.sql
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

2-lib-sess.php
<?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

site-a/3-index.php
<!-- (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

site-b/4-sync.php
<?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

site-b/5-verify.php
<?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 and expiry.
  • On accessing https://site-a.com, create a random hash and set the expiry 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.

 

LINKS & REFERENCES

 

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!

Leave a Comment

Your email address will not be published. Required fields are marked *