-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First-class session support in FastAPI #754
Comments
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 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 |
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.
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. 😞 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"} |
@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 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 |
Nit. #754 (comment) 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 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':
# ...
We can't implement these sessions as injectable-only, since they need to persist their data and append a |
I really think the ideal way to do this would be to more or less integrate the 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). |
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? |
Is this a good/recommended way of implementing secure cookie based authentication? |
Thanks for a simple solution you have offered right away, very useful but, |
could work |
@Technerder I'd like to add to this discussion that there is a way to use a JS SPA like React and work with httpOnly cookies for server-backed sessions in FastAPI. A tutorial I've made here for Django + React can easily be integrated with FastAPI: https://github.com/Andrew-Chen-Wang/SPA-with-sessions I'm also planning on writing tutorials for each SPA and Backend, including Rails if I want to learn it someday... |
@sm-Fifteen As high level example of the Authorization Code Flow:
Step number 3 is elusive badly documented and digging around making headache to everyone with shutting down 3rd party cookies by default.
So I was thinking if there is a way to allow the app using sessions also by local / distributed cache but for that some kind of session id is required. Is your proposal will be also be able to allow having this kind of session id? |
@sm-Fifteen I have included a basic app that has retrieve a session, check my session, and leave session. https://gist.github.com/jordanisaacs/a3633d0ba42758c4e3aa68c1fbc9231b edit: Since this feature has had a lot of responses and I plan on developing this further I created a repo for further developments. https://github.com/jordanisaacs/fastapi-sessions |
Hi @jordanisaacs - This is a good starting point even though I'm not sure how secure it is. I've also prepared a plug-in but working specifically with Microsoft MSAL which is very much useful for authenticating via MS graph and setting your own azure active directory tenant (AKA AAD B2C). It is not as generic as yours since I was looking to simplify the usage for using it. Anyway, It still has the same issue of the token redirect - but if you will use the OpenAPI documentation it is working well with the session object. https://github.com/dudil/fastapi_msal As you advance with your project I might use some concept of it in the plug-in, so thank you for that!!! @tiangolo - I hope you could find it useful for future references, AAD B2C is different than other OpenID authentication since it is providing you the end tenant (user management etc.) very much similar to Okta, was Cognito and Auth0, And I'm sure other people will find it useful as well. To be honest, with fastAPI it is very much a pleasure to use it and I hope I can make it even better in the future. |
Hi @dudil. Ya I wrote that as a quick proof of concept to see how one could implement this feature with FastAPI compatibility. That code should definitely not be used in any form for production. The copy problem from my understanding is also there with token based authentication (thus should be over https). For session cookies there needs to be defense against CSRF attacks which I am currently working on in my repo. Beyond that though I believe you start to move from simple first class support for sessions into a full authentication framework which I feel is out of scope. My current plan is to keep building out the sessions library so eventually I can use it in an authentication library. |
@dudil: For the OAuth flows, you'll probably want to look into Authlib. The OAuth2 Unrelated notes about using OAuth2 for logging in and opening sessionsAs for persisting the session, right now, I'm just using something like this:from authlib.integrations.starlette_client import StarletteRemoteApp, StartletteIntegration
from authlib.jose import jwt
from datetime import datetime
cookie_sec = APIKeyCookie(name="myapp_sess", auto_error=False)
oauth_remote = StarletteRemoteApp(
framework=StartletteIntegration("my_oauth_server"), name='my_oauth_server',
...
)
# Authorization Response (RFC6749 §4.1.2)
@auth_router.get('/custom_oauth_cb')
async def custom_oauth_callback(req: Request, cfg: MyAppSettings = Depends(get_settings)):
# Access Token Request (RFC6749 §4.1.3)
token = await oauth_remote.authorize_access_token(req)
user_uname = token.get('username') # Get some sort of user ID from your OAuth authorization response
token_header = {'alg': 'RS256'}
token_data = {
"iss": "MyApp",
# HACK: The JWT never expires
# TODO: Figure out a clean way to renew it
"sub": user_uname,
'iat': int(datetime.utcnow().timestamp()), # https://github.com/lepture/authlib/issues/277
}
api_key = jwt.encode(token_header, token_data, cfg.jwt_priv_key).decode('ascii')
res = RedirectResponse(url=cfg.frontend_path)
res.set_cookie(cookie_sec.model.name, api_key)
return res
# The parts you'll use `Depends()` on
async def get_session_username(jwt_id_token: Optional[str] = Depends(cookie_sec), cfg: MyAppSettings = Depends(get_settings)) -> Optional[str]:
if api_key is None: return None
try:
claims = jwt.decode(jwt_id_token, cfg.jwt_pub_key)
claims.validate()
uname = claims.get("sub", None)
return uname
except JoseError:
return None # The token is invalid, expired or incorrectly signed, so the user is not logged in
def get_current_user_unconditionally(username: Optional[str] = Depends(get_session_username), db_sess: Session = Depends(get_db_sess)) -> User:
pass # TODO: Get user from DB There's something extremely important you should realize when reading the code above: It is a massive hack to use OAuth to initiate a session login like this. The "Auth" in "OAuth" is for "Authorization", not "Authentication": The framework is only really meant to work to let users give programs the authorization to access your data from third-party services (like programs that need to see your list of Facebook friends or make Tweets on your behalf). If you only need to validate that the user exists once at login and then never really need to use the OAuth service for anything again, you have no reason/opportunity to make use of your OAuth tokens afterwards. Ideally you'd want something more akin to OpenID Connect for this sort of operation, if that's an option (though OIDC is a lot less common a feature than the plain OAuth it's based on). In any case, this issue is only related to the way the information is stored and accessed by FastAPI applications, not the login method per se. There's no reason why the system my original post is proposing wouldn't work with an authentication scheme like this. |
Getting this error, import |
The author of the pluggable sessions backend PR for Starlette mentioned elsewhere in this issue has made that code available as a middleware package: https://github.com/alex-oleshkevich/starsessions |
For anyone here who is interested I finally got around to finishing my redesign of fastapi-sessions. Took me a while to find a way to design it to handle mixed frontends (cookie, header, etc.), backends, and verification while still using dependency injection and thus the autogenerated openapi docs. Still have work to do on testing and writing more docs but I think the general design is in a much better spot :) |
Coming back to this issue after 2 years, given how it's one of the chosen few that hasn't been moved to discussions, I have a few more thoughts to give on this issue that I didn't have before:
I mentionned before using "non-field injectables" (like So having said all that, I would probably reccomend something closer to what fastapi-session does with regards to how sessions are configured in code, how it's backed by pydantic models, and how you can either get "shallow" (cookie-only) or "deep" (looked up in backend) session data depending on what's practical. |
Friends - Quick help pls. We are using Ping as our authentication partner. Code for successful authentication is in place but to verify the username from session I am clueless. Any idea?? |
Here is a slightly modified version of #754 (comment). To avoid using JWT, I used a randomly generated token as a session identifier. This approach might not be scalable, but I think some suitable situations exist.
|
Hi @ktaka-ccmp, this is a very common concept, but pay attention this specific design is exploit for session hijacking attacks. |
Hello @dudil, thanks for the feedback! I acknowledge the importance of being vigilant to prevent session hijacking attacks and appreciate your reminder. Here's what I plan to implement based on your suggestions and additional research:
Would there be other considerations or security measures you'd recommend? |
My Pleasure @ktaka-ccmp 🙏 Good Luck! |
Is your feature request related to a problem
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.
Describe alternatives you've considered
Using Starlette's SessionMiddleware
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 arequest.session
dict on theRequest
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.
The solution you would like
Ideally, I would like to see FastAPI adding some kind of
SessionCookie[T]
class tofastapi.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 aSet-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 likeMemorySessionStorage
,DatabaseSessionStorage
,FileSessionStorage
and so on.Additional context
Maybe something like this?
The text was updated successfully, but these errors were encountered: