FastAPI/Starlette's SessionMiddleware creates new session for every request - python

I need to create a session for authentication in the session_set endpoint. However, for some reason, the session is still being created in the session_info endpoint. How to make a session created only in session_set? Otherwise, I have a new session in the response with each request.
Here is my code:
import uvicorn
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="some-random-string", max_age=None)
#app.get("/a")
async def session_set(request: Request):
request.session["my_var"] = "1234"
return 'ok'
#app.get("/b")
async def session_info(request: Request):
my_var = request.session.get("my_var", None)
return my_var
if __name__ == '__main__':
uvicorn.run('http-session:app', port=5000, reload=True)

You could use a Middleware to override the session value in the Response cookies (check the documentation in Starlette as well) every time a new request arrives; hence, the session will remain the same.
Note: Remember to declare your custom middleware, after adding the SessionMiddleware to the app instance, as the order that endpoints/sub-applications are defined in your application matters, as described in this answer (see the relevant FastAPI documentation as well).
Working Example:
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="some-random-string")
#app.middleware("http")
async def some_middleware(request: Request, call_next):
response = await call_next(request)
session = request.cookies.get('session')
if session:
response.set_cookie(key='session', value=request.cookies.get('session'), httponly=True)
return response
#app.get("/a")
def func_a(request: Request):
request.session["my_var"] = "1234"
print(request.cookies.get('session'))
return 'OK'
#app.get("/b")
def func_b(request: Request):
my_var = request.session.get("my_var", None)
print(request.cookies.get('session'))
return my_var

Related

How to write a custom FastAPI middleware class

I have read FastAPI's documentation about middlewares (specifically, the middleware tutorial, the CORS middleware section and the advanced middleware guide), but couldn't find a concrete example of how to write a middleware class which you can add using the add_middleware function (in contrast to a basic middleware function added using a decorator) there nor on this site.
The reason I prefer to use add_middleware over the app based decorator, is that I want to write a middleware in a shared library that will be used by several different projects, and therefore I can't tie it to a specific FastAPI instance.
So my question is: how do you do it?
As FastAPI is actually Starlette underneath, you could use BaseHTTPMiddleware that allows you to implement a middleware class (you may want to have a look at this post as well). Below are given two variants of the same approach on how to do that, where the add_middleware() function is used to add the middleware class. Please note that is currently not possible to use BackgroundTasks (if that's a requirement for your task) with BaseHTTPMiddleware—check #1438 and #1640 for more details. Alternatives can be found in this answer and this answer.
Option 1
middleware.py
from fastapi import Request
class MyMiddleware:
def __init__(
self,
some_attribute: str,
):
self.some_attribute = some_attribute
async def __call__(self, request: Request, call_next):
# do something with the request object
content_type = request.headers.get('Content-Type')
print(content_type)
# process the request and get the response
response = await call_next(request)
return response
app.py
from fastapi import FastAPI
from middleware import MyMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
my_middleware = MyMiddleware(some_attribute="some_attribute_here_if_needed")
app.add_middleware(BaseHTTPMiddleware, dispatch=my_middleware)
Option 2
middleware.py
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class MyMiddleware(BaseHTTPMiddleware):
def __init__(
self,
app,
some_attribute: str,
):
super().__init__(app)
self.some_attribute = some_attribute
async def dispatch(self, request: Request, call_next):
# do something with the request object, for example
content_type = request.headers.get('Content-Type')
print(content_type)
# process the request and get the response
response = await call_next(request)
return response
app.py
from fastapi import FastAPI
from middleware import MyMiddleware
app = FastAPI()
app.add_middleware(MyMiddleware, some_attribute="some_attribute_here_if_needed")
A potential workaround for the BaseHTTPMiddleware bug raised by #Error - Syntactical Remorse, which seems to work for me at least, is to use partial and use a functional approach to your middleware definition:
middleware.py
from typing import Any, Callable, Coroutine
from fastapi import Response
async def my_middleware(request: Request, call_next: Callable, some_attribute: Any) -> Response:
request.state.attr = some_attribute # Do what you need with your attribute
return await call_next(request)
app.py
from functools import partial
from fastapi import FastAPI
from middleware import my_middleware
app = FastAPI()
my_custom_middleware: partial[Coroutine[Any, Any, Any]] = partial(my_middleware, some_attribute="my-app")
app.middleware("http")(my_custom_middlware)

session object in Fastapi similar to flask

I am trying to use session to pass variables across view functions in fastapi. However, I do not find any doc which specifically says of about session object. Everywhere I see, cookies are used. Is there any way to convert the below flask code in fastapi? I want to keep session implementation as simple as possible.
from flask import Flask, session, render_template, request, redirect, url_for
app=Flask(__name__)
app.secret_key='asdsdfsdfs13sdf_df%&'
#app.route('/a')
def a():
session['my_var'] = '1234'
return redirect(url_for('b'))
#app.route('/b')
def b():
my_var = session.get('my_var', None)
return my_var
if __name__=='__main__':
app.run(host='0.0.0.0', port=5000, debug = True)
Take a look at Starlette's SessionMiddleware. FastAPI uses Starlette under the hood so it is compatible.
After you register SessionMiddleware, you can access Request.session, which is a dictionary.
Documentation: SessionMiddleware
An implementation in FastAPI may look like:
#app.route("/a")
async def a(request: Request) -> RedirectResponse:
request.session["my_var"] = "1234"
return RedirectResponse("/b")
#app.route("/b")
async def b(request: Request) -> PlainTextResponse:
my_var = request.session.get("my_var", None)
return PlainTextResponse(my_var)

How to redirect to dynamic URL inside a FastAPI endpoint?

I'm doing a feature where the user on their profile page makes changes (not related to the user model). Everything is implemented through static HTML templates. I need the user to click on the button and return to the same page (i.e., their profile page) after processing the request.
Html template
<td>Accept</td>
endpoints.py
#router.get('/invite/{pk}/decline')
async def decline_event_invite(
request: Request,
pk: int,
user_id: str = Depends(get_current_user),
service: InviteService = Depends(),
):
await service.invite_decline(pk)
...
--> here I want redirect to user profile page
return RedirectResponse('DYNAMIC URL WITH ARGS')
profile.py
#router.get('/{pk}')
async def user_profile(
request: Request,
pk: int,
service: UserService = Depends()
):
user = await service.get_user_info(pk)
events_invites = await service.get_user_events_invite_list(pk)
return templates.TemplateResponse(
'profile.html',
context=
{
'request': request,
'user': user,
'events_invites': events_invites,
}
)
But I can't find anywhere how to do a redirect similar to the logic that applies to templates. For example:
Sender
You can use url_for() function and pass the (**kwargs) path parameters.
import uvicorn
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
import urllib
from fastapi import APIRouter
router = APIRouter()
templates = Jinja2Templates(directory="templates")
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int):
redirect_url = request.url_for('user_profile', **{ 'pk' : pk})
return RedirectResponse(redirect_url)
#router.get('/{pk}')
def user_profile(request: Request, pk: int):
return templates.TemplateResponse("profile.html", {"request": request, "pk": pk})
if __name__ == "__main__":
uvicorn.run(router, host='127.0.0.1', port=8000, debug=True)
To add query params
In case you had to pass query params as well, you could use the following code (make sure to import urllib). Alternatively, you could use the CustomURLProcessor, as described in this and this answer (which pretty much follows the same approach).
If the endpoint expected query params, for example:
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int, message: str):
pass
you could use:
redirect_url = request.url_for('user_profile', pk=pk)
parsed = list(urllib.parse.urlparse(redirect_url))
parsed[4] = urllib.parse.urlencode({**{ 'message' : "Success!"}})
redirect_url = urllib.parse.urlunparse(parsed)
or even use:
message = 'Success!'
redirect_url = request.url_for('user_profile', pk=pk) + f'?message={message}'
Update
Another solution would be to use Starlette's starlette.datastructures.URL, which now provides a method to include_query_params. Example:
from starlette.datastructures import URL
redirect_url = URL(request.url_for('user_profile', pk=pk)).include_query_params(message="Success!")

Flask-Login login_user() show encrypted cookie from Set-Cookie header

I am struggling with getting encoded, signed cookie from returned response at an endpoint where I had called login_user(my_user) before.
I was testing that code with curl -v each time - in the console I see < Set-Cookie: session=.eJwdjjkOAyEMAP9CncI2YMx..., but nothing printed by Python...
from flask import Flask
from flask_login import LoginManager, login_user, UserMixin
app = Flask(__name__)
login_manager = LoginManager(app)
app.config["SECRET_KEY"] = "abcd"
class User(UserMixin):
def __init__(self, identificator):
self.identificator = identificator
def get_id(self) -> int:
return int(self.identificator)
#login_manager.user_loader
def load_user(user_id):
return User.get_id(user_id)
#app.after_request
def ar(resp):
print(resp.headers.getlist("Set-Cookie")) # empty [] list here instead of cookies
return resp
#app.route("/test")
def test():
user = User(1234)
login_user(user)
return "something"
How to get the cookie (result of the login_user() call) which is going to be sent to the user?
The session is signed at the end of the response flow.
In my opinion there are two approaches you can take:
Create custom Flask application instance and extend the following method def process_response(self, response: Response) -> Response: to do the logic you need - see source to check exactly how the signing happens.
Create custom WSGI middleware and process that there - see docs for context.

Flask RESTFUL, creating a cookie from an endpoint (restful.Resource)

I'm currently working on creating a Cookie from an endpoint. As my backend and frontend only interacts via RESTful endpoints, is there anyway I can create a cookie when the frontend calls my backend's endpoint?
flask.make_response.set_cookie() doesn't seem to work for me. Also, I can't use app.route('/') to set my cookie either.
You can do this with Set-Cookie header returning with a response.
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
class HelloWorld(Resource):
def get(self):
return {'task': 'Hello world'}, 200, {'Set-Cookie': 'name=Nicholas'}
api.add_resource(HelloWorld, '/')
if __name__ == '__main__':
app.run(debug=True)
Setting the header in the response tuple is one of the standard approaches. However, keep in mind that the Set-Cookie header can be specified multiple times, which means that a python Dictionary won't be the most effective way to set the cookies in the response.
According to the flask docs the header object can also be initialized with a list of tuples, which might be more convenient in some cases.
Example:
from flask import Flask
from flask_restful import Api, Resource
app = Flask(__name__, static_url_path='')
api = Api(app)
class CookieHeaders(Resource):
def get(self):
# Will only set one cookie "age = 23"
return { 'message' : 'Made with dict'}, 200, { 'Set-Cookie':'name=john', 'Set-Cookie':'age=23' }
def post(self):
# Will set both cookies "name = john" and "age = 23"
headers = [ ('Set-Cookie', 'name=john'), ('Set-Cookie', 'age=23') ]
return { 'message' : ' Made with a list of tuples'}, 200, headers
api.add_resource(CookieHeaders, '/')
if __name__ == '__main__':
app.run(debug=True)
The GET call will only set 1 cookie (due to the lack of multi-key support in python dictionaries), but the POST call will set both.
Flask has a #after_this_request callback decorator. (see: http://flask.pocoo.org/docs/1.0/api/#flask.after_this_request)
so you can set your cookies in it
from flask import after_this_request
from flask_restful import Resource
class FooResource(Resource):
def get(self):
#after_this_request
def set_is_bar_cookie(response):
response.set_cookie('is_bar', 'no', max_age=64800, httponly=True)
return response
return {'data': 'foooo'}
or even
from flask import after_this_request, request
from flask_restful import Resource, abort
class FooResource(Resource):
def get(self):
self._check_is_bar()
return {'data': 'foooo'}
def _check_is_bar(self)
if request.cookies.get('is_bar') == 'yes':
abort(403)
#after_this_request
def set_is_bar_cookie(response):
response.set_cookie('is_bar', 'no', max_age=64800, httponly=True)
return response

Categories

Resources