Sharing Session Data between Mounted Applications #9318
-
Hi everyone 👋. I'm using Flask application within my FastAPI application by I have an example code down here for you all 👍 Here is the code for sharing cookie data, it's quite simple, I don't need to do anything special for this. I can access same cookie data with both applications. Flask Application: # app.py
from flask import Flask, jsonify, make_response, request
app = Flask(__name__)
@app.get("/")
def index():
return jsonify({"message": "Hello World"})
@app.get("/set-cookie")
def set_cookie():
res = make_response(jsonify({"message": "Cookie set"}))
res.set_cookie(key="application", value="flask")
return res
@app.get("/get-cookie")
def get_cookie():
return jsonify({"message": request.cookies.get("application")})
@app.get("/delete-cookie")
def delete_cookie():
res = make_response(jsonify({"message": "Cookie deleted"}))
res.delete_cookie(key="application")
return res FastAPI Application: # main.py
from fastapi import FastAPI, Request, Response
from starlette.middleware.wsgi import WSGIMiddleware
from app import app as flask_app
app = FastAPI(
title="FastAPI",
version="0.0.1",
)
app.mount("/flask-application", WSGIMiddleware(flask_app))
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
response = await call_next(request)
response.headers["X-Process-Time"] = "100"
print(response.headers)
return response
@app.get("/")
async def root(req: Request, res: Response):
return {"message": "Hello World"}
@app.get("/set-cookie")
async def set_cookie(req: Request, res: Response):
res.set_cookie(key="application", value="fastapi")
return {"message": "Cookie set"}
@app.get("/get-cookie")
async def get_cookie(req: Request, res: Response):
return {"message": req.cookies.get("application")}
@app.get("/delete-cookie")
async def delete_cookie(req: Request, res: Response):
res.delete_cookie(key="application")
return {"message": "Cookie deleted"} Here is the code for sharing session data which I couldn't make it work. I can't access same session data from both applications. Flask Application: # app.py
from flask import Flask, jsonify, session
import datetime
app = Flask(__name__)
config = {
"SECRET_KEY": "super-secret",
"PERMANENT_SESSION_LIFETIME": datetime.timedelta(days=7),
}
app.config.update(config)
@app.get("/")
def index():
return jsonify({"message": "Hello World"})
@app.get("/set-session")
def set_session():
session["application"] = "flask"
session.modified = True
return jsonify({"message": "Session set"})
@app.get("/get-session")
def get_session():
return jsonify({"message": session.get("application")})
@app.get("/delete-session")
def delete_session():
session.pop("application")
session.modified = True
return jsonify({"message": "Session deleted"})
@app.get("/get-session-data")
def get_session_data():
print(session.keys())
print(session.items())
print(session.values())
return {"message": "session"} FastAPI Application: # main.py
from fastapi import FastAPI, Request, Response
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.wsgi import WSGIMiddleware
from app import app as flask_app
app = FastAPI(
title="FastAPI",
version="0.0.1",
)
app.mount("/flask-application", WSGIMiddleware(flask_app))
app.add_middleware(
SessionMiddleware,
secret_key="super-secret",
)
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
response = await call_next(request)
response.headers["X-Process-Time"] = "100"
print(response.headers)
return response
@app.get("/")
async def root(req: Request, res: Response):
return {"message": "Hello World"}
@app.get("/set-session")
async def set_session(req: Request, res: Response):
req.session.update({"application": "fastapi"})
return {"message": "Session set"}
@app.get("/get-session")
async def get_session(req: Request, res: Response):
return {"message": req.session.get("application")}
@app.get("/delete-session")
async def delete_session(req: Request, res: Response):
req.session.pop("application")
return {"message": "Session deleted"}
@app.get("/get-session-data")
async def get_session_data(req: Request, res: Response):
return {"message": req.session.items()} Thank you for reading 🙏, eager to see some answers. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 4 replies
-
It's been a long time since I posted this discussion, no one commented and even reacted, it's probably more of a Starlette issue instead of FastAPI. Sorry about that! I figured it out today. I had to go deep but I did. It was a fun road. Hear me out! After two months from posting this discussion, I discovered two packages: fastapi-flask-auth and flask-session-decoder from the same author. I opened an issue for a related problem... The developer was very helpfull but it wasn't what I want. I'm thinking now, I don't even remember what I wanted back then... I toke a look at the source code of Why am I telling this like a story, I don't know... Like I was saying, I figured it out today. The way FastAPI stores session data in cookies is different than Flask's approach. I first tried to re-implement a Here is the gist: Decode and Encode Flask's session cookie. Great for testing purposes; only the secret key is needed I sat down and wrote a middleware called import sys
import typing
from hashlib import sha1
from typing import Union
from itsdangerous import BadSignature, URLSafeTimedSerializer
from starlette.datastructures import MutableHeaders, Secret
from starlette.requests import HTTPConnection
from starlette.types import ASGIApp, Message, Receive, Scope, Send
if sys.version_info >= (3, 8): # pragma: no cover
from typing import Literal
else: # pragma: no cover
from typing_extensions import Literal
class FlaskSigner:
def __init__(
self,
secret_key: Union[str, bytes],
salt: bytes = b"cookie-session",
) -> None:
self.serializer = URLSafeTimedSerializer(
secret_key=secret_key if isinstance(secret_key, bytes) else secret_key.encode("utf-8"),
salt=salt,
signer_kwargs={
"key_derivation": "hmac",
"digest_method": sha1,
},
)
def sign(
self,
value: Union[str, bytes],
) -> str:
return self.serializer.dumps(value)
def unsign(
self,
signed_value: Union[str, bytes],
):
return self.serializer.loads(signed_value)
class FlaskSessionMiddleware:
def __init__(
self,
app: ASGIApp,
secret_key: typing.Union[str, Secret],
session_cookie: str = "session",
max_age: typing.Optional[int] = 14 * 24 * 60 * 60, # 14 days, in seconds
path: str = "/",
same_site: Literal["lax", "strict", "none"] = "lax",
https_only: bool = False,
salt: bytes = b"cookie-session",
) -> None:
self.app = app
# self.signer = FlaskSigner(secret_key)
self.session_cookie = session_cookie
self.max_age = max_age
self.path = path
self.security_flags = "httponly; samesite=" + same_site
self.serializer = URLSafeTimedSerializer(
secret_key=secret_key if isinstance(secret_key, bytes) else secret_key.encode("utf-8"),
salt=salt,
signer_kwargs={
"key_derivation": "hmac",
"digest_method": sha1,
},
)
if https_only: # Secure flag can be used with HTTPS only
self.security_flags += "; secure"
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] not in ("http", "websocket"): # pragma: no cover
await self.app(scope, receive, send)
return
connection = HTTPConnection(scope)
initial_session_was_empty = True
session_initial = {}
if self.session_cookie in connection.cookies:
data = connection.cookies[self.session_cookie].encode("utf-8")
try:
session = self.serializer.loads(data)
session_initial = session.copy()
scope["session"] = session
initial_session_was_empty = False
except BadSignature:
scope["session"] = {}
else:
scope["session"] = {}
async def send_wrapper(message: Message) -> None:
if message["type"] == "http.response.start":
if scope["session"] and scope["session"] == session_initial:
...
# Session data is not changed.
elif scope["session"] and scope["session"] != session_initial:
# Session data is changed - We have session data to persist.
data = self.serializer.dumps(scope["session"])
headers = MutableHeaders(scope=message)
header_value = (
"{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( # noqa E501
session_cookie=self.session_cookie,
data=data,
path=self.path,
max_age=f"Max-Age={self.max_age}; " if self.max_age else "",
security_flags=self.security_flags,
)
)
headers.append("Set-Cookie", header_value)
elif not initial_session_was_empty:
# The session has been cleared.
headers = MutableHeaders(scope=message)
header_value = (
"{session_cookie}={data}; path={path}; {expires}{security_flags}".format( # noqa E501
session_cookie=self.session_cookie,
data="null",
path=self.path,
expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ",
security_flags=self.security_flags,
)
)
headers.append("Set-Cookie", header_value)
await send(message)
await self.app(scope, receive, send_wrapper) It wasn't working perfectly at the begining because the You can try it yourself, just change the I didn't hard tested it, but it seems to be working. I'm not sure if it's the best way to do it but it's working. I'm open to suggestions. Please note I'm not a security expert. I'm just a developer who is trying to learn new things. |
Beta Was this translation helpful? Give feedback.
It's been a long time since I posted this discussion, no one commented and even reacted, it's probably more of a Starlette issue instead of FastAPI. Sorry about that!
I figured it out today. I had to go deep but I did. It was a fun road. Hear me out!
After two months from posting this discussion, I discovered two packages: fastapi-flask-auth and flask-session-decoder from the same author. I opened an issue for a related problem... The developer was very helpfull but it wasn't what I want. I'm thinking now, I don't even remember what I wanted back then...
I toke a look at the source code of
SessionMiddleware
class. It wasn't complicated at all. I played with it for a while and figure how i…