One of the most common ways to store user session data in modern web development is through HTTP cookies. However, despite their widespread use, cookies can still be a source of confusion—especially when dealing with cross-origin requests.
Over the years, browsers have implemented a plethora of features to ensure that cookies cannot be easily exploited for malicious purposes. Attributes like HttpOnly, Secure, and SameSite, combined with stringent cross-origin policies, are designed to make improper use of cookies more challenging.
Cookies could also raise significant privacy concerns. They can enable tracking across multiple websites, often without the user’s knowledge, by embedding identifiers into ads or scripts loaded on various pages. This has led to growing scrutiny and efforts to limit third-party cookie usage. All the major browsers like Firefox, Google Chrome, Safari,Microsoft Edge or Brave have taken steps to address these concerns by implementing stricter cookie policies.
After carefully considering all the security and privacy concerns surrounding cookies, we must also acknowledge that they are not just useful but often essential for modern web functionality. To make the most of their benefits while minimizing risks, it’s crucial to understand how cookies work and how to use them responsibly. Let’s dive in!
In this article, we’ll explore:
- How cookies work
- The role of CORS
1. The Basics: How Cookies Work
A cookie is a small piece of data stored in the browser, typically set by servers using an HTTP header called Set-Cookie in the response. Once stored, cookies are automatically included in subsequent requests, but only under certain conditions. They play a crucial role in session management, making them invaluable for web development.
Each cookie has an expiration date: it can either persist for a specified duration or be temporary, lasting only until the browser is closed. Cookies are sent from the user’s browser to the backend only when specific conditions are met, such as when the cookie’s domain, path, and SameSite policy align with the request. This mechanism ensures controlled and secure data exchange.
Below, you will find a scheme that illustrates these steps.
As we mentioned, once a cookie is "implanted" in the browser, it will be sent back to the backend with subsequent requests—but only under certain conditions. Below is a flowchart that illustrates how the browser determines whether a cookie should be sent or not:
Did you know that the first browser to support cookies was Netscape Navigator, introduced in 1994, followed by Internet Explorer in 1995? It’s fascinating to look back at how these early innovations shaped the web we use today. I'm so nostalgic for that early Internet era!
1.1 Common Cookie Attributes
Attribute | Description | Example Value |
---|---|---|
Name-Value |
Key-value pair representing the cookie data. | session_id=abc123 |
HttpOnly |
Prevents JavaScript from reading the cookie, mitigating XSS attacks. | HttpOnly=True |
Secure |
Ensures the cookie is only sent over HTTPS connections. | Secure=True |
Domain |
Defines the domain(s) where the cookie is valid. | api.mojatools.com |
Path |
Specifies the URL paths for which the cookie is valid. | / |
SameSite |
Controls cross-origin behavior: None , Lax , or Strict . |
SameSite=None |
I want to give some details about HttpOnly and Samesite:
HttpOnly
When a cookie is set with the HttpOnly attribute, it becomes inaccessible to JavaScript running in the browser. This means the cookie cannot be retrieved, modified, or manipulated using client-side code, such as document.cookie.
The primary purpose of the HttpOnly flag is to mitigate Cross-Site Scripting (XSS) attacks.
In an XSS attack, an attacker injects malicious JavaScript into a webpage. If a cookie isn't protected with the HttpOnly attribute, the attacker could try to steal sensitive information, such as session cookies, potentially hijacking a user's session.
Combining HTTPOnly with the Secure flag is always a good idea to ensure cookies are sent only over HTTPS.
SameSite
The SameSite attribute provides a way to control when cookies should be included in requests initiated by other domains. By setting this attribute, you can specify the circumstances under which a browser should send cookies, thereby enhancing security and privacy.
It is designed to mitigate Cross-Site Request Forgery (CSRF) attacks
SameSite=Strict
*Cookie Behavior: Cookies from mojatools.com will not be sent when the request originates from mojalab.com (cross-origin request).
*Use Case: Only works for same-origin scenarios. Useful for maximum security.
*Example:The user visits mojalab.com and triggers a request to mojatools.com (e.g., via an API call).
*Result: The cookies are not sent because the request originates from a different domain (mojatools.com).
SameSite=Lax
*Behavior: Cookies are sent for same-site requests and top-level GET navigation requests (e.g., clicking a link to your site from another domain).
*Use Case: Balances usability and security. Suitable for most session cookies or general site functionality.
*Example: User clicks a link on mojalab.com to visit mojatools.com (top-level navigation).
*Result: The cookies are sent because top-level navigation is allowed under SameSite=Lax.
*However: If mojalab.com makes an API call to mojatools.com using JavaScript (e.g., fetch() or XHR), the cookies are not sent because it's a cross-origin call.
SameSite=None
*Behavior: Cookies are sent for all requests, including cross-site ones, but must include Secure=true (requires HTTPS).
*Use Case: Essential for cross-origin functionality, such as third-party APIs, single sign-on (SSO), or external services.
*Example: User logs in on mojatools.com, and a session cookie is set with SameSite=None and Secure=true.
mojalab.com makes an API call to mojatools.com using JavaScript (fetch() or XHR).
- Result: The cookies are sent, allowing the API call to work correctly.
1.2 Setting a Cookie in Flask
Let's start to see how to create a response that will send a cookie from our server
This snippet of code is written in Python using the Flask web framework.
from flask import make_response, jsonify
resp = make_response(jsonify({"message": "Cookie issued"}))
resp.set_cookie(
"session_id",
"my_cookie_value",
httponly=True,
secure=True,
samesite="None",
path="/",
domain="api.mojatools.com"
)
return resp
Setting | Purpose |
---|---|
httponly=True |
Protects against JavaScript access (security). |
secure=True |
Ensures cookies are sent over HTTPS only. |
samesite="None" |
Allows cross-origin cookie usage. |
path="/" |
Makes the cookie valid site-wide. |
domain="..." |
Ties the cookie to the API domain. |
1.3 Sending Back Cookies with Fetch API
To demonstrate how cookies are sent back to the server during an API call, let’s consider an example in JavaScript where the front end is hosted on mojalab.com. It needs to interact with an API hosted on mojatools.com, and we just have a cookie previously implanted by the server (as in the previous flask example). This setup is typical in applications where the frontend and backend reside on different domains, and the backend uses cookies for session management or authentication.
// Making a fetch request to the API at mojatools.com
fetch("https://api.mojatools.com/some-test-endpoint", {
method: "GET", // HTTP method (e.g., GET, POST, PUT, etc.)
credentials: "include", // Ensures cookies are included in the request
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // Parse the response as JSON
})
.then((data) => {
console.log("API Response:", data); // Handle the JSON data from the API
})
.catch((error) => {
console.error("Error fetching data:", error); // Handle any errors
});
In this code, the credentials: "include"
option is crucial—it instructs the browser to include cookies in the request, even for cross-origin calls. The server at mojatools.com must also be configured to accept cross-origin requests and include appropriate CORS headers, such as Access-Control-Allow-Origin
and Access-Control-Allow-Credentials
. Using this approach, the browser automatically sends the cookie to the backend, provided it satisfies conditions like the cookie's domain
, path
, Secure
, and SameSite
attributes.
2. Cross-Origin Requests: CORS and Cookies
2.1 What Is CORS?
Cross-Origin Resource Sharing (CORS) is a mechanism that allows restricted resources on a web server to be accessed from a different domain. Configuring CORS properly is essential to avoid the blocking of API calls when working with cross-domain interactions.
When a browser makes a cross-origin request, it follows these steps:
1. The Browser Makes an Initial HTTP Request
- When a client-side application (e.g., a web app on
mojalab.com
) tries to make a request to an API on another domain (e.g.,mojatools.com
), the browser begins the process. - Depending on the type of request, it may proceed directly or perform a "preflight" check first.
2. Preflight Request (OPTIONS Method)
- For certain types of cross-origin requests (e.g., requests with custom headers or methods like
PUT
orDELETE
), the browser sends a preflight request to the server before the actual request is made. - The preflight request:
- Uses the HTTP method
OPTIONS
. - Contains special headers, such as:
Origin
: The domain making the request (e.g.,mojalab.com
).Access-Control-Request-Method
: Specifies the method of the subsequent request (e.g.,POST
).Access-Control-Request-Headers
: Lists any custom headers that will be sent.
- Uses the HTTP method
This is the example of a Preflight request header:
OPTIONS /test-api HTTP/2
Host: api.mojatools.com
...
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Origin: https://mojalab.com
....
3. Server Responds to the Preflight Request
- The server at
mojatools.com
evaluates whether to allow the request based on the CORS policy. - It sends back a response with headers like:
Access-Control-Allow-Origin
: Specifies the allowed origin(s) (e.g.,https://mojalab.com
or*
for all domains).Access-Control-Allow-Methods
: Lists HTTP methods allowed for the actual request (e.g.,GET
,POST
).Access-Control-Allow-Headers
: Lists allowed custom headers.Access-Control-Allow-Credentials
: Iftrue
, indicates that cookies or authorization headers can be included in the request.
This is an example of the response from the server
HTTP/2 200
...
access-control-allow-origin: https://mojalab.com
access-control-allow-credentials: true
access-control-allow-headers: content-type
access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
....
4. The Browser Evaluates the Preflight Response
- The browser checks the server’s preflight response to see if it matches the requested conditions.
- If the response headers match the request requirements, the browser proceeds with the actual HTTP request.
- If not, the browser blocks the request and raises a CORS error.
5. The Actual HTTP Request
- If the preflight check is successful or unnecessary (e.g., for simple requests like
GET
orPOST
without custom headers), the browser makes the actual HTTP request to the server. - The request may include cookies or authorization headers if
credentials: "include"
is used.
6. Server Responds to the Actual Request
- The server processes the request and sends back the response along with the necessary CORS headers.
- For example:
HTTP/2 200 OK Access-Control-Allow-Origin: https://mojalab.com Access-Control-Allow-Credentials: true Content-Type: application/json
7. The Browser Delivers the Response to the Client
- If the server's response satisfies the CORS policy, the browser allows the client-side code to access the response.
- Otherwise, the response is blocked, and the browser raises a CORS error.
2.2 Credentials in Cross-Origin Requests
Is important to know that to send cookies in cross-origin requests you have to:
- Client-side: As in our example use
withCredentials: true
(jQuery) orcredentials: "include"
(Fetch API). - Server-side: Configure CORS headers to receive the cookie.
Flask Example:
from flask_cors import CORS
CORS(app, resources={r"/*": {"origins": "https://mojalab.com", "supports_credentials": True}})
3. Troubleshooting Checklist
If you're having trouble figuring out why your cookies aren't being sent to your backend, follow this checklist; I hope it helps.
Step | Action |
---|---|
Check DevTools | Inspect Set-Cookie headers and Application storage. |
Verify HTTPS | Ensure Secure cookies are sent over HTTPS. |
Confirm CORS Settings | Check Access-Control-Allow-Origin and credentials configuration. |
Adjust SameSite Setting |
Use SameSite=None for cross-origin cookies. |
Test Without Extensions | Temporarily disable Ghostery or uBlock Origin to isolate the issue. |
4. Conclusion
Cookies are an essential tool for session management and data storage but require careful handling to ensure security and compatibility. Understanding attributes like HttpOnly
, Secure
, and SameSite
, combined with proper CORS configuration and fallback mechanisms, will create a robust user experience.
Disclaimer: At MojaLab, we aim to provide accurate and useful content, but hey, we’re human (well, mostly)! If you spot an error, have questions, or think something could be improved, feel free to reach out—we’d love to hear from you. Use the tutorials and tips here with care, and always test in a safe environment. Happy learning!