A cookie-dependent API, an anti-corruption layer in a legacy application, cross-site requests, and separate backend and frontend teams. How to make it all work?
🔥😎 That's salty.
Preface
Imagine a situation. You work in a team consisting of frontend and backend developers. You need the frontend developers to be able to debug their code to get the project up and running.
If the frontend team needs to send AJAX requests to another domain (which points to your API) things get more complicated. And if your API sets and waits for cookies, things get catastrophically complicated. The browser will block the sending of cookies, the PHP session "won't work" on its own, and the AJAX request will also be blocked because that's how modern browsers work. This is a security feature.
👀
Sometimes you have to deal with old monolithic applications that you have to put on the right rails - microservices. But you can't break the application down into smaller services at once, so you have to depend on cookies, sessions, and so on. This happens even when you create a new API, because your old application still depends on cookies and PHP sessions inside, so you have to implement cookie support in your endpoints... That's true. It's called an anti-corruption layer or backward compatibility. But cookies in APIs can be painful to develop and work with, especially in remote/separate teams.
So, how to configure the environment if the frontend application is running on one domain, for example "localhost:3000", and the internal API application is running on a different domain ("your-team.local.com"), how to make sure that AJAX requests are sent, cookies are set, and the PHP session works?
Solution
#1. Let's make your cookies bad
We need to make sure that the AJAX requests sent from the frontend include cookies, even if they are set for a different domain. How?
🖐️
This approach is called third-party cookies. They are mostly used to collect advertising statistics. But that's not so important right now.
To do this, on the backend, we need to set the cookie parameter SameSite=none. But only this parameter is not enough for the cookie to work, we also need to set the parameter Secure.
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure
Don't ask me. It's the goddamn specs. You can read the proof here.
#2. Securing the API with an HTTPS certificate
The problem with cookies is that using the Secure option in a cookie implies that the cookie has to be sent over a secure connection, HTTPS.
Thus, your test domain, e.g. "your-team.local.com", which points to your API, must have a valid or self-signed certificate. You can generate a self-signed certificate and use it with a simple command.
sudo apt-get update &&
sudo apt-get install -y openssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -subj "/C=NA/ST=LocalPlace/L=City/O=OrgName/OU=IT Department/CN=your-team.local.com" -keyout server.key -out server.crt
Then use this certificate in Apache or Nginx.
😉
Self-signed certificates are difficult to work with and are only suitable for dev environments, as they require that certificate validation is disabled on all clients. Thus, if you do not have serious network constraints in this regard, you can use: https://letsencrypt.org/.
#3. Make your frontend send cookies to another domain
Suppose your frontend framework (or library) works with Axios.
For our Axios to work with your API, we need to disable SSL certificate validation and configure Axios to send cookies.
📡
Remember that you must disable the "rejectUnauthorized" option in production, this setting is ONLY suitable for the dev environment
#4. Overwriting the default session cookie for backward compatibility
Again about cookies. If your backend sends a session cookie in addition to the regular cookies, but you don't know where it happens or can't change it because of breaking changes (yes, for example, you use an old framework version like Yii2 or another monolithic solution). We'll override the session cookie! Put this part at the end of your code, for example in the footer.
$domain = ".{$domain}";
session_start([
'cookie_domain' => $domain
]);]
header("Set-Cookie: PHPSESSID=" . session_id() . "; path=/; domain={$domain}; Secure; HttpOnly; SameSite=None", false);
So, even if a cookie was set earlier, you overwrite its existing value, but with the additional parameter "SameSite=None". This move allows you to send session cookies for cross-site requests.
Believe me, finding and implementing this solution with sessions was not easy.
#4. Allow your frontend to send cross-site AJAX requests
By default, the browser blocks any cross-site request. You encountered the problem "Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at". You did...right? 🙂
So we need to set up CORS policies, this can be done at the PHP level (code snippet from Stack Overflow, an example)
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With'
];
if ($request->isMethod('OPTIONS'))
{
return response()->json('{"method":"OPTIONS"}', 200, $headers);
}
$response = $next($request);
foreach($headers as $key => $value)
{
$response->header($key, $value);
}
Or here is a variant of the nginx config:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
}
That's it! From now on, your frontend application hosted on one domain can send cross-site requests to a cookie-dependent API hosted on another domain.
Please let me know if you don't understand any part of the code or if you want more details and explanations.
Please subscribe to my site to receive new posts!