CORS and Cookies with Vue.js and Flask
1. What is CORS?
-
CORS (Cross-Origin Resource Sharing) is a mechanism that allows a web application running at one origin (domain) to access resources from another origin.
-
By default, browsers enforce the Same-Origin Policy:
- A script loaded from
http://localhost:8080cannot make AJAX requests tohttp://localhost:5000unless the server explicitly allows it. - π The primary purpose of CORS is to control which domains can access resources on a server. It is not about encryption, compression, or authentication β those are handled by other mechanisms (HTTPS, gzip, sessions/JWT, etc.).
- A script loaded from
-
Example:
- Vue frontend runs at:
http://localhost:8080 - Flask backend runs at:
http://localhost:5000 - Without CORS enabled on Flask, the request will be blocked.
- Vue frontend runs at:
2. What are Cookies?
-
Cookies are small pieces of data stored in the clientβs browser.
-
They can be used for:
- Authentication (storing session IDs / JWT tokens)
- Preferences (dark mode, language, etc.)
- Tracking
-
Important cookie flags:
HttpOnlyβ prevents JavaScript access.Secureβ only sent over HTTPS.SameSite:"Lax"β Sent for top-level navigation (safe for dev)."Strict"β Sent only if frontend and backend are same origin."None"β Always sent, requiresSecure=True.
πͺ Important Cookie Flags
1. HttpOnly
-
Meaning: If set, JavaScript (
document.cookie) cannot access the cookie. -
Why: Protects cookies (like session IDs or tokens) from XSS (Cross-Site Scripting) attacks.
-
Use case: Always set
HttpOnly=Truefor sensitive cookies like authentication/session tokens. -
Example:
resp.set_cookie("auth_token", "abc123", httponly=True)
2. Secure
-
Meaning: Cookie will only be sent over HTTPS connections.
-
Why: Prevents exposure of cookies over insecure HTTP.
-
Use case: In production, always set
Secure=Truefor authentication cookies. In local development, you may temporarily useFalse. -
Example:
resp.set_cookie("auth_token", "abc123", secure=True)
3. SameSite
-
Meaning: Controls whether the cookie can be sent with cross-site requests (important for CORS).
-
Options:
Lax(default in modern browsers) β Cookie is sent for same-site requests and top-level navigations (like clicking a link), but not for cross-origin AJAX/fetch calls.Strictβ Cookie is only sent if request originates from the same site. Blocks all cross-site usage (very secure but restrictive).Noneβ Cookie is sent in all cross-site requests, but only ifSecure=True(required by Chrome, Firefox, Edge).
-
Use case:
- If your frontend (
http://localhost:5173) and backend (http://localhost:5000) are on different origins β you must useSameSite=None+ Secure=True.
- If your frontend (
-
Example:
resp.set_cookie("auth_token", "abc123", samesite="None", secure=True)
4. Domain
-
Meaning: Defines which domain(s) can access the cookie.
-
If not set β defaults to the domain that set the cookie.
-
Example:
resp.set_cookie("auth_token", "abc123", domain=".example.com")β Will be accessible from
sub1.example.com,sub2.example.com, etc.
5. Path
-
Meaning: Defines the URL path for which the cookie is valid.
-
Default β
/(accessible everywhere in the site). -
Example:
resp.set_cookie("auth_token", "abc123", path="/admin")β Cookie will only be sent for requests starting with
/admin.
Example (Recommended for Auth Cookies)
For cross-origin requests (different frontend & backend domains):
resp.set_cookie(
"auth_token",
"abc123",
max_age=3600, # Expires in 1 hour (simpler than datetime)
httponly=True, # Protect from XSS
secure=True, # Only send over HTTPS (production)
samesite="None", # Required for cross-origin cookies
path="/",
)
For same-site requests (localhost dev, frontend & backend on same origin):
from datetime import datetime, timedelta
resp.set_cookie(
"auth_token",
"abc123",
expires=datetime.utcnow() + timedelta(minutes=1), # Explicit expiry date
httponly=True, # Protect from XSS
secure=False, # OK for local HTTP dev
samesite="Lax", # Safe for same-site requests
path="/",
)
Note: Both max_age (relative seconds) and expires (absolute datetime) achieve cookie expiry. Pick whichever reads clearer to you.
Debugging Tip
To check stored cookies:
-
Open DevTools β Application β Storage β Cookies in Chrome/Edge/Firefox.
-
Look for:
- Name/Value
- Domain
- Path
- HttpOnly
- Secure
- SameSite
3. Flask Setup (Backend with CORS + Cookies)
Install dependencies
pip install flask flask-cors
Flask app (cors.py)
from flask import Flask, jsonify, request, make_response
from flask_cors import CORS
from datatime import datetime, timedelta
app = Flask(__name__)
# Enable CORS with credentials support
CORS(app, supports_credentials=True,
origins=["http://127.0.0.1:5500", "http://localhost:5500"])
@app.route("/set-cookie")
def set_cookie():
resp = make_response(jsonify({"message": "Cookie is set!"}))
resp.set_cookie(
"user_token",
"abc123",
# max_age=10 # seconds
expires=datetime.utcnow() + timedelta(minutes=1), # deletes in 1 minute
httponly=True, # JS cannot read this cookie
# secure=False, # True in production with HTTPS
secure=True,
samesite=None,
# samesite="Lax" # Works locally for cross-site cookies
)
return resp
@app.route("/get-cookie")
def get_cookie():
token = request.cookies.get("user_token")
if token:
return jsonify({"message": "Cookie retrieved!", "token": token})
return jsonify({"message": "No cookie found!"}), 404
@app.route("/delete-cookie")
def delete_cookie():
resp = make_response({"msg": "Cookie deleted"})
resp.delete_cookie("user_token")
return resp
if __name__ == "__main__":
app.run(debug=True)
Why this code?
CORS(... supports_credentials=True ...)β allows cookies to be sent across origins.set_cookie()β backend sends aSet-Cookieheader β browser stores the cookie locally.get_cookie()β backend reads cookies that the browser automatically attaches to the request.
4. Vue.js Setup (Frontend)
Vue Component (app.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<title>CORS + Cookies Demo</title>
</head>
<body>
<div id="app">
<h1>CORS + Cookies Demo</h1>
<button @click="setCookie">Set Cookie</button>
<button @click="getCookie">Get Cookie</button>
<!-- Create a delete button ( task ) -->
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: "#app",
data: {
message: "hi",
},
methods: {
async setCookie() {
try {
const res = await fetch("http://127.0.0.1:5000/set-cookie", {
method: "GET",
credentials: "include", // include cookies
});
const data = await res.json();
this.message = data.message;
} catch {
this.message = "Error setting cookie!";
}
},
async getCookie() {
try {
const res = await fetch("http://127.0.0.1:5000/get-cookie", {
credentials: "include",
});
const data = await res.json();
this.message = data.message + " (Token: " + data.token + ")";
} catch {
this.message = "Error retrieving cookie!";
}
},
async deleteCookie() {
try {
const res = await fetch("http://127.0.0.1:5000/delete-cookie", {
method: "GET",
credentials: "include",
});
const data = await res.json();
this.message = data.msg;
} catch {
this.message = "Error deleting cookie!";
}
},
},
});
</script>
</body>
</html>
Why this code?
credentials: "include"β ensures cookies are included in cross-origin requests.fetch(...).json()β parses the JSON response from Flask.- Buttons call
setCookie,getCookie, anddeleteCookiemethods β triggers the API calls.
Server vs File (Critical for development):
- Wrong: Double-click
app.htmlor drag it into browser β opens asfile://URL.- Cookies will NOT work (browser blocks them on file origin).
- Correct: Use Live Server extension (right-click β "Open with Live Server") β opens as
http://localhost:5500.- Cookies will work (trusted HTTP origin).
This is the most common reason cookies fail in local dev. The code is correct; the origin was wrong.
5. Where Cookies Are Stored
-
After clicking Set Cookie, Flask sends:
Set-Cookie: user_token=abc123; HttpOnly; SameSite=Lax -
Browser stores this cookie under DevTools β Application β Cookies β http://127.0.0.1:5000.
-
On the next request to
http://127.0.0.1:5000/get-cookie, the browser automatically attaches:Cookie: user_token=abc123
The frontend (Vue) does not manually send the cookie β the browser handles it automatically when credentials: "include" is set.
6. Frontend vs Backend Roles
-
Frontend (Vue):
- Initiates requests (
fetchwith credentials). - Displays messages from backend.
- Does not directly manage secure cookies.
- Initiates requests (
-
Backend (Flask):
- Controls cookie creation (
resp.set_cookie). - Validates cookies on requests (
request.cookies.get). - Decides whether a cookie is valid and what data to send back.
- Controls cookie creation (
7. How It Works β Flow
- User clicks Set Cookie β Vue calls Flask β Flask responds with
Set-Cookie. - Browser stores
user_tokenlocally. - User clicks Get Cookie β Vue calls Flask with credentials β browser attaches
user_tokenβ Flask reads it and returns token.
8. Key Notes
- CORS + Credentials Rule: When using
supports_credentials=True, you must specify explicit origin(s) in CORS (e.g.,origins=["http://127.0.0.1:5500"]). Wildcard origin (*) is rejected by browsers for credentialed requests. - Development (same-site local setup): use
samesite="Lax"andsecure=False(HTTP is fine). - Development (cross-origin or testing): use
samesite="None",secure=Falselocally only. - Production: use
samesite="None",secure=True(HTTPS required for SameSite=None).
What Counts as a Different Origin?
Browsers consider these different origins (cookies won't send between them without CORS + SameSite=None):
| Frontend | Backend | Same Origin? | Why? |
|---|---|---|---|
http://localhost:5500 |
http://localhost:5000 |
β No | Different port |
http://127.0.0.1:5500 |
http://127.0.0.1:5000 |
β No | Different port |
http://localhost:5500 |
http://127.0.0.1:5500 |
β No | Different domain (localhost β 127.0.0.1) |
http://localhost:5500 |
https://localhost:5500 |
β No | Different protocol (HTTP β HTTPS) |
http://localhost:5500/app |
http://localhost:5500/api |
β Yes | Same protocol, domain, port (path doesn't matter) |
β
How to fix host mismatch: Use the same domain everywhere. Pick either localhost or 127.0.0.1 for both frontend and backend, then CORS + cookies will work.
πΉ Ways to Auto-Delete Cookies (Extra)
-
Session Cookies (Default)
- If you donβt set
expiresormax_age, the cookie will auto-delete when the browser is fully closed.
resp.set_cookie("auth_token", "abc123")Auto-deletes when user closes the browser.
- If you donβt set
-
Time-Limited Cookies (Max-Age / Expires)
- Set an explicit lifetime (e.g., 10 seconds).
resp.set_cookie( "auth_token", "abc123", max_age=10 # seconds )Auto-deletes after 10 seconds (browser will drop it).
Or with
expires:from datetime import datetime, timedelta resp.set_cookie( "auth_token", "abc123", expires=datetime.utcnow() + timedelta(minutes=1) )Auto-deletes after 1 minute.
-
Manual Delete (Server-Controlled)
- If you want the user to log out immediately, you must explicitly call
delete_cookie. - Auto-delete only works based on time or session, not on events like "logout".
- If you want the user to log out immediately, you must explicitly call
πΉ Important Notes
- Session cookies vanish on browser close.
- Max-Age/Expires cookies vanish automatically after the set duration.
- If you need "logout now" behavior β you still need to delete_cookie.
π So yes, auto-deletion is possible, but only time-based or session-based. For instant removal, youβll need explicit deletion code.