All of the security schemas currently supported by FastAPI rely on some sort of "client-server synergy" , where, for instance, the client is expected to know and remember an OAuth token or the user credentials to be sent via headers. This works fairly well for single-page applications, but if you need to integrate authentication to an app that uses templates, keeping track of that authentication data becomes a challenge. Most applications would use server-side sessions to deal with this, but FastAPI doesn't really have a system to deal with sessions right now.
While Starlette's SessionMiddleware
is mentionned a number of times in the FastAPI documentation, it does not integrate very well with the framework itself . What it does is that it adds a request.session
dict on the Request
object that lets the backend store and retreive information from it, and just before the response get sent, that dict is serialized, combined to a timestamp, signed, converted into base 64 and appended as a cookie. The client is then expected to send theat cookie back so that the server so that information can be decoded and used. This is what the Django doc describes as the cookie-based approach.
The problem with all this is that the entire process happens outside of what FastAPI can handle, and therefore does not appear in the generated OpenAPI documentation as an authentication schema.
Having read the source for that middleware and the documentation for itsdangerous, I also understand that this kind of "session blob" authentication method isn't really supported by OpenAPI, since all supported auth methods are expected to use constants to handle authentication.
Ideally, I would like to see FastAPI adding some kind of SessionCookie[T]
class to fastapi.security
, that would register a cookie-based API key authentication method (which is what Swagger reccomands, since sessions are out of scope for the OpenAPI spec). Those "API keys" would be session tokens, much like the It should also register that routes that depend on that security schema may reply with a Set-Cookie
header.
The question of how that data would be persisted afterwards is an open one. Having a one-size-fits-all implementation as the only one available could be constraining, so there's always the option of a fastapi.security.sessions
namespace containing things like MemorySessionStorage
, DatabaseSessionStorage
, FileSessionStorage
and so on.
Maybe something like this?
from fastapi import Depends, FastAPI
from fastapi.security.sessions import SessionCookie, MemorySessionStorage
from pydantic import BaseModel
from datetime import timedelta
app = FastAPI()
class SessData(BaseModel):
# BaseModel so it can be serialized and stored properly
uname: str
security = SessionCookie[SessData](
name='fastapi_sess',
expires=timedelta(hours=1).
backend=MemorySessionStorage(),
auto_error=False
)
@app.get('/secure/rm_rf/{path:path}')
def secure_thing(path: str, session: Optional[SessData] = Depends(security)):
if session is not None and session.uname == 'root':
# ...
Related to #212
Looks good to me
@prostomarkeloff: I know this is off-topic, but I thought LGTM was what you're supposed to say when reviewing patches and pull requests? What would that mean in the context of a feature request?
@sm-Fifteen hmm. I wanted say that i am interested in this feature :)
In general I like this idea, because I think a lot of issue have been raised by people looking for this feature, especially in the context of a basic browser-driven interface.
That said, for maintainability reasons, I would much prefer an implementation that was ultimately powered by features in Starlette, where FastAPI just provided a cleaner dependency injection API and OpenAPI integration, and possibly using generics like SessionCookie
as you wrote it above to get more of a typed interface (possibly with validation).
In particular, I think it would be much preferable for FastAPI to essentially wrap the pluggable session backends that are a part of this PR, assuming it is eventually merged.
@sm-Fifteen thoughts?
Separately, I think it would be great if we had (optional) support for generics in many places where Starlette has an untyped interface. In particular, in addition to the SessionCookie
as you've described it, I really wish there was an easy way to generically specify the types of request.state
and request.app.state
. I have some hacks in my own code to accomplish this, but maybe there is a clean way to unify all of these...
That said, for maintainability reasons, I would _much_ prefer an implementation that was ultimately powered by features in Starlette, where FastAPI just provided a cleaner dependency injection API and OpenAPI integration, and possibly using generics like
SessionCookie
as you wrote it above to get more of a typed interface (possibly with validation).In particular, I think it would be much preferable for FastAPI to essentially wrap the pluggable session backends that are a part of this PR, assuming it is eventually merged.
I was about to argue until I checked the linked issue, and wow, if that isn't some perfect timing! Yeah, it would probably make more sense to try and reduce the portion of this that's maintained on FastAPI's side of things, especially since Starlette has much to gain from having the kind of session support we would be extending.
Separately, I think it would be great if we had (optional) support for generics in many places where Starlette has an untyped interface. In particular, in addition to the
SessionCookie
as you've described it, I really wish there was an easy way to generically specify the types ofrequest.state
andrequest.app.state
. I have some hacks in my own code to accomplish this, but maybe there is a clean way to unify all of these...
Yeah, I've had to do a bit of React lately, and while I have conflicting opinions about it, one of the thing I've found extremely useful is component state typing, which solves the issue I had with AngularJS of having scopes that would end up turning into vaguely defined dictionnary soup. Session data is similarily arbitrary, so I figure having some way of specifying what type its contained data should have would be a big plus in making this feature integrate well with the whole FastAPI stack.
It's already in place. More or less like the rest of the security tools. And it's compatible with the rest of the parts, integrated with OpenAPI (as possible), but probably most importantly, with dependencies.
It's just not properly documented yet. :disappointed:
But still, it works :rocket: e.g.
from fastapi import FastAPI, Form, HTTPException, Depends
from fastapi.security import APIKeyCookie
from starlette.responses import Response, HTMLResponse
from starlette import status
from jose import jwt
app = FastAPI()
cookie_sec = APIKeyCookie(name="session")
secret_key = "someactualsecret"
users = {"dmontagu": {"password": "secret1"}, "tiangolo": {"password": "secret2"}}
def get_current_user(session: str = Depends(cookie_sec)):
try:
payload = jwt.decode(session, secret_key)
user = users[payload["sub"]]
return user
except Exception:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid authentication"
)
@app.get("/login")
def login_page():
return HTMLResponse(
"""
<form action="/login" method="post">
Username: <input type="text" name="username" required>
<br>
Password: <input type="password" name="password" required>
<input type="submit" value="Login">
</form>
"""
)
@app.post("/login")
def login(response: Response, username: str = Form(...), password: str = Form(...)):
if username not in users:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid user or password"
)
db_password = users[username]["password"]
if not password == db_password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid user or password"
)
token = jwt.encode({"sub": username}, secret_key)
response.set_cookie("session", token)
return {"ok": True}
@app.get("/private")
def read_private(username: str = Depends(get_current_user)):
return {"username": username, "private": "get some private data"}
@tiangolo: Using API key cookies to manage session data would be a bit of a hack at best. The JWT only works so long as it can fit all your session data in there (remember that RFC 6265 does not require user agents to support individual cookies larger than 4096 bytes, and browsers don't usually go over that limit) or if you have a separate session storage backend, and it's then up to the user to implement said storage backend. Compare with my original post and with what encode/starlette#499 is trying to implement. Compare also to how PHP handles sessions by generating session keys and creating temporary sess_{$sess_key}
files (that contain a serialized copy of the $_SESSION
superglobal) while sending you the session key to use as your session cookie or to Django's swappable session storage backends (linked in my original post).
My initial example was specifically about having a mix of API routes and server-side rendered templates using the same session data. Having a single-page app talking to the API works well, but with the lacking session support we have now, it's hard to transition back and forth between the SPA and the rendered templates.
My humble opinion: I feel that FastAPI is kind of a stateless thing by design (look at how use of app.state
is discouraged. If such a session storage is required it will definitely require integration with Redis or something similar. In this case using a third-party library to handle these integration (e.g. aiocache) is much better than implementing everything from the beginning.
Nit. https://github.com/tiangolo/fastapi/issues/754#issuecomment-585386650 snippet needs to be amended, since
user = users[payload["sub"]]
return user
actually returns
{"password": "secret1"} # or whatever
instead of
username: str
So assuming we wanted to integrate the content from the upstream Starlette pull request more or less as-is into FastAPI, how would we proceed? The proposed upstream changes merely adapt SessionMiddleware to lazily create an active object living in scope['session']
that will be saved before its session cookie to the request headers if the object happened to get modified. This means the majority of the logic for this still resides in a middleware running before the FastAPI app, and causes a few issues.
Keep in mind I'm still aiming for roughly the interface proposed in the original post:
@app.get('/secure/rm_rf/{path:path}')
def secure_thing(path: str, session: Optional[SessData] = Depends(security)):
if session is not None and session.uname == 'root':
# ...
Session
wrapper to work like other security types, where the cookie name, maximum session duration and other factors are specified on creation, this would require passing this same information to the middleware when it is attached to the app.Session
type is an active dict-like object, which monitors its own internal mutations to skip saving its own data unless needed.We can't implement these sessions as injectable-only, since they need to persist their data and append a Set-Session
header to the outgoing response before http.response.start
is sent. Context-manager dependencies can't do this because they need to stay open until the background tasks have completed (see #661), which is long after the headers have already been sent. If FastAPI doesn't opt to reimplement something equivalent to that middleware as a first-class Depends
-able type with the extra side-effects, the only valid way to implement this is to have a middleware in front of the app, with all the caveats stated above.
I really think the ideal way to do this would be to more or less integrate the Session
class from the upstream PR and then treat it as a non-field injectable, to ensure that sessions are properly controlled by the framework and are only loaded/created when needed.
This would bypass having to split one feature between an (upstream) middleware and the core framework, allow for better integration with the dependency system, avoid all of the issues I enumerated in my last comment (including not working well with typing, whether through Pydantic or anything else, which would be a huge benefit), avoid storing an active session object in the connection scope in the first place (which really clashes with the main purpose of the connection scope, since serializing and deserializing it makes the session inside inert and non-functional), allow for integration with the OpenAPI documentation, and wouldn't clash with mounted sub-applications whether or not they need to access any sort of session info.
Having tried to get the upstream Starlette middleware to work with current FastAPI (which involved rewriting the whole middleware because of the split between the middleware proper and the FastAPI injectable), I'm really not sure the upstream PR would be that helpful to enable session integration in FastAPI (in a way that doesn't clash with the rest of the framework, that is).
I have thus far created access_token for each user loggingIn. Now, what can be done for creating sessions for each user to store more data??
@app.post("/login_basic")
async def login_basic(request:Request, username: str = Form(...), password: str = Form(...) ):
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": username}, expires_delta=access_token_expires
)
token = jsonable_encoder(access_token)
response = RedirectResponse(url="/loggedIn")
response.set_cookie(
"Authorization",
value=f"Bearer {token}",
httponly=True,
max_age=1800
)
return response
Stuffing session data in an unencrypted JWT inside a cookie is not a good idea. It potentially leads to information disclosure and should be avoided as a practice. I think FastAPI could really benefit from solid session management - but this could of course easily be an external package. FastAPI contains all the ingredient to build it.
@st3fan: Not sure about how I would go about making it work as an external package, given my comments above. How would you structure a FastAPI-specific session plugin?
It's already in place. More or less like the rest of the security tools. And it's compatible with the rest of the parts, integrated with OpenAPI (as possible), but probably most importantly, with dependencies.
It's just not properly documented yet. 馃槥
But still, it works 馃殌 e.g.
from fastapi import FastAPI, Form, HTTPException, Depends from fastapi.security import APIKeyCookie from starlette.responses import Response, HTMLResponse from starlette import status from jose import jwt app = FastAPI() cookie_sec = APIKeyCookie(name="session") secret_key = "someactualsecret" users = {"dmontagu": {"password": "secret1"}, "tiangolo": {"password": "secret2"}} def get_current_user(session: str = Depends(cookie_sec)): try: payload = jwt.decode(session, secret_key) user = users[payload["sub"]] return user except Exception: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid authentication" ) @app.get("/login") def login_page(): return HTMLResponse( """ <form action="/login" method="post"> Username: <input type="text" name="username" required> <br> Password: <input type="password" name="password" required> <input type="submit" value="Login"> </form> """ ) @app.post("/login") def login(response: Response, username: str = Form(...), password: str = Form(...)): if username not in users: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid user or password" ) db_password = users[username]["password"] if not password == db_password: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid user or password" ) token = jwt.encode({"sub": username}, secret_key) response.set_cookie("session", token) return {"ok": True} @app.get("/private") def read_private(username: str = Depends(get_current_user)): return {"username": username, "private": "get some private data"}
Is this a good/recommended way of implementing secure cookie based authentication?
Most helpful comment
It's already in place. More or less like the rest of the security tools. And it's compatible with the rest of the parts, integrated with OpenAPI (as possible), but probably most importantly, with dependencies.
It's just not properly documented yet. :disappointed:
But still, it works :rocket: e.g.