Skip to content

Commit fb0ff97

Browse files
author
“bchou9”
committed
Fix a bunch of CORS related issues for deployment
1 parent 8bbf27b commit fb0ff97

File tree

4 files changed

+65
-14
lines changed

4 files changed

+65
-14
lines changed

backend/app.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,17 @@ def handle_rate_limit_exception(e):
101101
local_regexes = [r"^https?://localhost(:\d+)?$", r"^https?://127\.0\.0\.1(:\d+)?$"]
102102

103103
cors_origins = explicit_allowed + local_regexes
104-
CORS(app, supports_credentials=True, origins=cors_origins)
104+
105+
CORS(app,
106+
resources={r"/*": {
107+
"origins": cors_origins,
108+
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
109+
"allow_headers": ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
110+
"expose_headers": ["Content-Type", "Authorization"],
111+
"supports_credentials": True,
112+
"max_age": 3600
113+
}}
114+
)
105115

106116
def origin_allowed(origin):
107117
"""Return True if the provided origin string is allowed by explicit allowed
@@ -126,17 +136,17 @@ def add_cors_headers(response):
126136
This complements flask-cors and guards against cases where exception paths
127137
or other middleware may return responses without the proper headers.
128138
"""
139+
if response.headers.get("Access-Control-Allow-Origin"):
140+
return response
141+
129142
try:
130143
origin = request.headers.get("Origin")
131144
if origin and origin_allowed(origin):
132145
response.headers["Access-Control-Allow-Origin"] = origin
133146
response.headers["Access-Control-Allow-Credentials"] = "true"
134-
else:
135-
fallback = explicit_allowed[0] if explicit_allowed else "http://localhost:10008"
136-
response.headers.setdefault("Access-Control-Allow-Origin", fallback)
137-
response.headers.setdefault("Access-Control-Allow-Credentials", "true")
138-
response.headers.setdefault("Access-Control-Allow-Headers", "Content-Type,Authorization")
139-
response.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
147+
response.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-Requested-With,Accept"
148+
response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
149+
response.headers["Access-Control-Expose-Headers"] = "Content-Type,Authorization"
140150
except Exception:
141151
pass
142152
return response
@@ -165,28 +175,32 @@ def handle_all_exceptions(e):
165175
if origin and origin_allowed(origin):
166176
resp.headers["Access-Control-Allow-Origin"] = origin
167177
resp.headers["Access-Control-Allow-Credentials"] = "true"
178+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-Requested-With,Accept"
179+
resp.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
180+
resp.headers["Access-Control-Expose-Headers"] = "Content-Type,Authorization"
168181
else:
169182
fallback = explicit_allowed[0] if explicit_allowed else "http://localhost:10008"
170183
resp.headers.setdefault("Access-Control-Allow-Origin", fallback)
171184
resp.headers.setdefault("Access-Control-Allow-Credentials", "true")
172-
resp.headers.setdefault("Access-Control-Allow-Headers", "Content-Type,Authorization")
173-
resp.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
185+
resp.headers.setdefault("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With,Accept")
186+
resp.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
187+
resp.headers.setdefault("Access-Control-Expose-Headers", "Content-Type,Authorization")
174188
return resp
175189
except Exception:
176190
# If even the error handler fails, return a minimal JSON response
177191
logger.exception("Error while handling exception")
178192
out = make_response(json.dumps({"status": "error", "message": "Fatal error"}), 500)
179193
out.headers.setdefault("Access-Control-Allow-Origin", "http://localhost:10008")
180194
out.headers.setdefault("Access-Control-Allow-Credentials", "true")
181-
out.headers.setdefault("Access-Control-Allow-Headers", "Content-Type,Authorization")
195+
out.headers.setdefault("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With,Accept")
182196
out.headers.setdefault("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
183197
out.headers["Content-Type"] = "application/json"
184198
return out
185199

186200

187201
from flask_socketio import SocketIO
188202
import services.socketio_service as socketio_service
189-
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
203+
socketio = SocketIO(app, cors_allowed_origins=cors_origins, async_mode="threading")
190204
socketio_service.socketio = socketio
191205
socketio_service.register_socketio_handlers()
192206

backend/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
REFRESH_TOKEN_COOKIE_NAME = os.getenv("REFRESH_TOKEN_COOKIE_NAME", "rescanvas_refresh")
3636
REFRESH_TOKEN_COOKIE_SECURE = os.getenv("REFRESH_TOKEN_COOKIE_SECURE", "False") == "True"
3737
REFRESH_TOKEN_COOKIE_SAMESITE = os.getenv("REFRESH_TOKEN_COOKIE_SAMESITE", "Lax")
38+
REFRESH_TOKEN_COOKIE_PARTITIONED = os.getenv("REFRESH_TOKEN_COOKIE_PARTITIONED", "False") == "True"
3839

3940
ROOM_MASTER_KEY_B64 = os.getenv("ROOM_MASTER_KEY_B64")
4041
if not ROOM_MASTER_KEY_B64:

backend/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ services:
3737
- JWT_SECRET=${JWT_SECRET:-dev-insecure-change-me}
3838
- OPENAI_API_KEY=${OPENAI_API_KEY}
3939
- ANALYTICS_ENABLED=${ANALYTICS_ENABLED:-True}
40+
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
41+
- REFRESH_TOKEN_COOKIE_SECURE=${REFRESH_TOKEN_COOKIE_SECURE:-True}
42+
- REFRESH_TOKEN_COOKIE_SAMESITE=${REFRESH_TOKEN_COOKIE_SAMESITE:-None}
43+
- REFRESH_TOKEN_COOKIE_PARTITIONED=${REFRESH_TOKEN_COOKIE_PARTITIONED:-True}
4044
volumes:
4145
- ./logs:/app/logs
4246
depends_on:

backend/routes/auth.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from config import (
99
JWT_SECRET, JWT_ISSUER, ACCESS_TOKEN_EXPIRES_SECS, REFRESH_TOKEN_EXPIRES_SECS,
1010
REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_COOKIE_SECURE, REFRESH_TOKEN_COOKIE_SAMESITE,
11+
REFRESH_TOKEN_COOKIE_PARTITIONED,
1112
RATE_LIMIT_LOGIN_HOURLY, RATE_LIMIT_REGISTER_HOURLY, RATE_LIMIT_REFRESH_HOURLY
1213
)
1314
from middleware.auth import require_auth, validate_request_data
@@ -55,6 +56,37 @@ def _find_valid_refresh_token(token_hash):
5556
})
5657
return doc
5758

59+
def _set_refresh_cookie(response, token_value, max_age):
60+
"""Set refresh token cookie with all necessary attributes including Partitioned.
61+
62+
The Partitioned attribute is required for third-party cookies in modern browsers
63+
to prevent cross-site tracking.
64+
"""
65+
cookie_parts = [
66+
f"{REFRESH_TOKEN_COOKIE_NAME}={token_value}",
67+
f"Max-Age={max_age}",
68+
f"Path=/",
69+
]
70+
71+
if REFRESH_TOKEN_COOKIE_SECURE:
72+
cookie_parts.append("Secure")
73+
74+
cookie_parts.append("HttpOnly")
75+
76+
if REFRESH_TOKEN_COOKIE_SAMESITE:
77+
cookie_parts.append(f"SameSite={REFRESH_TOKEN_COOKIE_SAMESITE}")
78+
79+
if REFRESH_TOKEN_COOKIE_PARTITIONED:
80+
cookie_parts.append("Partitioned")
81+
82+
expires = datetime.utcnow() + timedelta(seconds=max_age)
83+
expires_str = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
84+
cookie_parts.append(f"Expires={expires_str}")
85+
86+
cookie_string = "; ".join(cookie_parts)
87+
response.headers.add("Set-Cookie", cookie_string)
88+
return response
89+
5890
@auth_bp.route("/auth/register", methods=["POST"])
5991
@limiter.limit(f"{RATE_LIMIT_REGISTER_HOURLY}/hour")
6092
@validate_request_data({
@@ -96,7 +128,7 @@ def register():
96128
expires_at = datetime.utcnow() + timedelta(seconds=REFRESH_TOKEN_EXPIRES_SECS)
97129
_store_refresh_token(user["_id"], h, expires_at)
98130
resp = make_response(jsonify({"status":"ok","token": access, "user": {"username": user["username"], "walletPubKey": user.get("walletPubKey")}}), 201)
99-
resp.set_cookie(REFRESH_TOKEN_COOKIE_NAME, raw_refresh, httponly=True, secure=REFRESH_TOKEN_COOKIE_SECURE, samesite=REFRESH_TOKEN_COOKIE_SAMESITE, max_age=REFRESH_TOKEN_EXPIRES_SECS)
131+
_set_refresh_cookie(resp, raw_refresh, REFRESH_TOKEN_EXPIRES_SECS)
100132
return resp
101133

102134
@auth_bp.route("/auth/login", methods=["POST"])
@@ -130,7 +162,7 @@ def login():
130162
expires_at = datetime.utcnow() + timedelta(seconds=REFRESH_TOKEN_EXPIRES_SECS)
131163
_store_refresh_token(user["_id"], h, expires_at)
132164
resp = make_response(jsonify({"status":"ok","token": access, "user": {"username": user["username"], "walletPubKey": user.get("walletPubKey")}}))
133-
resp.set_cookie(REFRESH_TOKEN_COOKIE_NAME, raw_refresh, httponly=True, secure=REFRESH_TOKEN_COOKIE_SECURE, samesite=REFRESH_TOKEN_COOKIE_SAMESITE, max_age=REFRESH_TOKEN_EXPIRES_SECS)
165+
_set_refresh_cookie(resp, raw_refresh, REFRESH_TOKEN_EXPIRES_SECS)
134166
return resp
135167

136168
@auth_bp.route("/auth/refresh", methods=["POST"])
@@ -150,7 +182,7 @@ def refresh():
150182
expires_at = datetime.utcnow() + timedelta(seconds=REFRESH_TOKEN_EXPIRES_SECS)
151183
_store_refresh_token(user["_id"], new_h, expires_at)
152184
resp = make_response(jsonify({"status":"ok","token": access}))
153-
resp.set_cookie(REFRESH_TOKEN_COOKIE_NAME, new_raw, httponly=True, secure=REFRESH_TOKEN_COOKIE_SECURE, samesite=REFRESH_TOKEN_COOKIE_SAMESITE, max_age=REFRESH_TOKEN_EXPIRES_SECS)
185+
_set_refresh_cookie(resp, new_raw, REFRESH_TOKEN_EXPIRES_SECS)
154186
return resp
155187

156188
@auth_bp.route("/auth/logout", methods=["POST"])

0 commit comments

Comments
 (0)