Skip to content
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

Open
sm-Fifteen opened this issue Nov 29, 2019 · 33 comments
Open

First-class session support in FastAPI #754

sm-Fifteen opened this issue Nov 29, 2019 · 33 comments
Labels
feature New feature or request reviewed

Comments

@sm-Fifteen
Copy link
Contributor

sm-Fifteen commented Nov 29, 2019

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 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.

The solution you would like

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.

Additional context

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':
        # ...
@sm-Fifteen sm-Fifteen added the feature New feature or request label Nov 29, 2019
@sm-Fifteen
Copy link
Contributor Author

Related to #212

@prostomarkeloff
Copy link
Contributor

Looks good to me

@sm-Fifteen
Copy link
Contributor Author

@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?

@prostomarkeloff
Copy link
Contributor

@sm-Fifteen hmm. I wanted say that i am interested in this feature :)

@dmontagu
Copy link
Collaborator

dmontagu commented Nov 29, 2019

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...

@sm-Fifteen
Copy link
Contributor Author

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 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...

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.

@tiangolo
Copy link
Member

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"}

@sm-Fifteen
Copy link
Contributor Author

@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.

@phy25
Copy link

phy25 commented Feb 13, 2020

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.

@jessewiles
Copy link

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

@sm-Fifteen
Copy link
Contributor Author

sm-Fifteen commented Jun 19, 2020

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':
        # ...
  1. The middleware and the injectable session object would need to be aware of each other in various ways.
  • Assuming we would like our injectable 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.
  • The middleware could be initialized with the injectable as a parameter and extract the relevent cookie information from there.
  1. The proposed Session type is an active dict-like object, which monitors its own internal mutations to skip saving its own data unless needed.
  • This seems to me like it's going to be an issue if the session is also meant to wrap a Pydantic type, since I'm guessing pydantic would prevent the active session object from seeing mutations done to its data.
  1. The middleware as it is only allows for one "session" to exist for the entire app, whereas dependency injection would suggest that any number of sessions can be passed as needed.
  • I've never seen any sort of framework allow for multiple independent "session" types to coexist before but I'm not aware of any way users could be prevented to attempt that with a mixed middleware-dependable approach. The middleware would need to be capable of creating a separate lazily-loaded session object in the connection scope for each one.

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.

@sm-Fifteen
Copy link
Contributor Author

sm-Fifteen commented Jun 26, 2020

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).

@st3fan
Copy link

st3fan commented Aug 13, 2020

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.

@sm-Fifteen
Copy link
Contributor Author

@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?

@Technerder
Copy link

Technerder commented Aug 19, 2020

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?

@arsalanses
Copy link

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"}

Thanks for a simple solution you have offered right away, very useful but,
how can we delete the cookie when the user want's to log out? :(

@ternaus
Copy link

ternaus commented Jan 13, 2021

@arsalanses

@app.get("/logout")
def logout(response: Response):
    response.delete_cookie("session")
    return {"ok": True}

could work

@Andrew-Chen-Wang
Copy link

Andrew-Chen-Wang commented Jan 15, 2021

@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...

@dudil
Copy link

dudil commented Mar 27, 2021

@sm-Fifteen
Will your proposal be able to handle also the authorization code grant flow?
(If you feel this is not relevant please let me know and I'll post this as new issue)

As high level example of the Authorization Code Flow:

  1. App is requesting authorization code (usually this is exposed as /login path)
  2. The authorization server handles client authentication via separate browser session
  3. Once authorized it is redirected with a code response --> here is main issue with session cookie!
  4. Code is sent to the authorization server and if authorized app is retrieving the long awaited access_token

Step number 3 is elusive badly documented and digging around making headache to everyone with shutting down 3rd party cookies by default.
I'll explain better since it has two possible solutions:

  1. Redirect to web and then app (this is how it works with the OpenAPI documentation generated) -
    Code authorization is redirected to the
    web part which sends it via form parts to the app.
    In this case the session is coming from the original web part, so saving data to the session is working fine.
  2. Redirect to the app - asking the authorization server to redirect to an app path (usually /token).
    In this case the session is coming from the authorization server so saving data to the session will not be persistent to followup calls...

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?
@tiangolo - I am not very familiar with Uvicorn etc. is there an API which allow us to retrieve a session id from the ASGI server?

@jordanisaacs
Copy link

jordanisaacs commented Apr 3, 2021

@sm-Fifteen
I have done a basic implementation of what my understanding of this feature ask is as I also have a use for it. It has OpenAPI and dependency injection compatibility based off of the APIKeyCookie. And uses a pydantic data model with an abstract SessionBackend for storage of the data (based off of the starlette pull request). I have only implemented in memory but it should not be difficult to extend it.

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

@dudil
Copy link

dudil commented Apr 4, 2021

Hi @jordanisaacs - This is a good starting point even though I'm not sure how secure it is.
Since lots is actually relied on the session ID - copy it and use it might be proved not that difficult.

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.
Using Starlette as a base framework did helped rewrite a very much synchronous library to be Async - which is amazing for itself :)

@jordanisaacs
Copy link

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.

@sm-Fifteen
Copy link
Contributor Author

@sm-Fifteen
Will your proposal be able to handle also the authorization code grant flow?
(If you feel this is not relevant please let me know and I'll post this as new issue)

As high level example of the Authorization Code Flow:

1. App is requesting authorization code (usually this is exposed as /login path)

2. The authorization server handles client authentication via separate browser session

3. Once authorized it is redirected with a code response --> **here is main issue with session cookie!**

4. Code is sent to the authorization server and if authorized app is retrieving the long awaited access_token

Step number 3 is elusive badly documented and digging around making headache to everyone with shutting down 3rd party cookies by default.
I'll explain better since it has two possible solutions:

1. Redirect to web and then app (this is how it works with the OpenAPI documentation generated) -
   Code authorization is redirected to the
   _web part_ which sends it via form parts to the app.
   In this case the session is coming from the original web part, so saving data to the session is working fine.

2. Redirect to the app - asking the authorization server to redirect to an app path (usually /token).
   In this case the session is coming from the authorization server so saving data to the _session will not be persistent to followup calls_...

@dudil: For the OAuth flows, you'll probably want to look into Authlib. The OAuth2 protocol framework has a lot of moving parts and implementing any part of it yourself is likely to be an endless source of trouble. The discussion over #335 might also be of interest to you.

Unrelated notes about using OAuth2 for logging in and opening sessions As 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.

@FIRDOUS-BHAT
Copy link

fastapi.security.sessions

Getting this error, import fastapi.security.sessions cannot be resolved

@abrookins
Copy link

abrookins commented Jul 29, 2021

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

@jordanisaacs
Copy link

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 :)

@tiangolo tiangolo changed the title [FEATURE] First-class session support in FastAPI First-class session support in FastAPI Feb 24, 2023
@sm-Fifteen
Copy link
Contributor Author

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:

  • The upstream implementation of SessionMiddleware has an unintended side-effect that's especially obvious with API servers where the session cookie is set to a new value (updated timestamp and signature) for every query made to the server, so long as the session cookies was set in the query. Unlike Firefox and Edge, Chrome will acknowledge these Set-Cookie responses if they are performed by fetch() calls, even if the cookie's value has changed between the request and the response, and even if the query returns after the page has been navigated away from. This means slow fetch routes can cause cookies to unexpectedly change, which is especially problematic during OAuth redirections. Any new solution should probably ensure that cookie updates are done sparingly.
  • The signed cookies used by itsdangerous' TimestampSigner are basically proto-JWTs (field order is data payload, binary timestamp and HMAC-SHA1 signature instead of JWT header, data payload and a signature in whatever format the header specified), so we might as well replace those with actual JWTs instead.
    • This could be a good way to solve the issue of cookies constantly being updated (see previous point), because JWTs don't need to store the timestamp at which they have been signed.
  • There is value in having some session data stored in a JWT, even if there is an actual session backend in place, since this saves you from having to fetch all session data from the database on every request even if you only need to know the username or permissions of the current user. It basically acts as a partial session cache. Having data in your "frontend session cookie" doesn't preclude having an actual session store, and this is something that only clicked in my head now that I'm reading the examples from @jordanisaacs' library (which I can't believe I didn't do until now).
    • A good way to explain this in the doc/tutorial might be to compare them to ski lift tickets, which are small and thus meant to carry very little information, but just enough for someone to be able to tell from a glance that the person carrying it is allowed to use the lifts on that day. Most of the times, all the information needed is on there, and for the times where you need more data than what can fit (or that you wouldn't want everyone to see, like the credit card number you used to pay for the tickets), you can have a barcode (a session ID, in my analogy) to look it up in your system (session backend) if needed. If that's not a use case you need and all your information can fit on there, then you can forgo the barcode entirely and just have the tickets be your only source of session data.

I mentionned before using "non-field injectables" (like Request and Response, detected by their specified class in the function signature) for the session, so as to avoid users trying to have 5 different session cookies and guide them towards having just one, but the more I think about it, the more I think that may be misguided. Third party libraries (like AuthLib) will sometimes need to store their own session data that's mostly independent from yours, and it may not be a bad thing to have those use separate session cookies and maybe even backends (maybe I want my user sessions to be backed by a database, but my Oauth login flow can probably work just fine as an in-memory dict).

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.

@itsrchaitu
Copy link

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??
So my logic is: if authenticated user in session, proceed to loading application else do not allow. need if anything simple like flask stuff is in place for fastapi too

@ktaka-ccmp
Copy link

ktaka-ccmp commented Mar 5, 2024

Here is a slightly modified version of #754 (comment).

To avoid using JWT, I used a randomly generated token as a session identifier.
Sessions are stored in a dictionary, which should be replaced by a Redis store or an SQL database in production.

This approach might not be scalable, but I think some suitable situations exist.
What do you think?

from fastapi import FastAPI, Form, HTTPException, Depends
from fastapi.security import APIKeyCookie
from fastapi.responses import Response, HTMLResponse
from fastapi import status
import secrets

app = FastAPI()

cookie_sec = APIKeyCookie(name="session")

users = {"aaa": {"password": "aaaa"}, "bbb": {"password": "bbbb"}}
sessions = {}

def create_session(username):
    # Generate token, i.e. session_id without using JWT.
    token = secrets.token_urlsafe(32)
    # Session stored in a dictionary. It can be replaced by Redis or Database easily.
    sessions[token] = username
    print("Sessions", sessions)
    return token

def delete_session(token):
    del sessions[token]
    print("Sessions", sessions)
    return {"Removed token": token}

def get_current_user(token: str = Depends(cookie_sec)):
    try:
        user = sessions[token]
        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 = create_session(username)
    response.set_cookie("session", token)
    return {"ok": True}

@app.get("/logout")
def logout(response: Response, token: str = Depends(cookie_sec)):
    delete_session(token)
    response.delete_cookie("session")
    return {"ok": True}

@app.get("/private")
def read_private(username: str = Depends(get_current_user)):
    return {"username": username, "private": "get some private data"}

@dudil
Copy link

dudil commented Mar 5, 2024

Here is a slightly modified version of #754 (comment).

To avoid using JWT, I used a randomly generated token as a session identifier. Sessions are stored in a dictionary, which should be replaced by a Redis store or an SQL database in production.

This approach might not be scalable, but I think some suitable situations exist. What do you think?

Hi @ktaka-ccmp, this is a very common concept, but pay attention this specific design is exploit for session hijacking attacks.
If you would use a short-lived token instead of the user object this would fit more as a proper architecture for implementation.

@ktaka-ccmp
Copy link

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:

  • Set cookies with httponly=True, secure=True, and samesite="Lax" attributes.
  • Ensure session cookies are short-lived and introduce a mechanism to regenerate session identifiers regularly.
  • Implement a CSRF token mechanism to safeguard against Cross-Site Request Forgery when necessary.
  • Enforce the use of HTTPS across all production sites.
  • Properly configure the Content-Security-Policy header to mitigate the risk of content injection attacks.

Would there be other considerations or security measures you'd recommend?

@dudil
Copy link

dudil commented Mar 5, 2024

My Pleasure @ktaka-ccmp 🙏
YMMV but as long as you are aware and looking into potential pitfalls you are on the right track.
I believe @tiangolo had provided a very good and educated tutorial covering many other potential issues as part of the FastAPI tutorial: https://fastapi.tiangolo.com/advanced/security/

Good Luck!

@ktaka-ccmp
Copy link

@dudil Thanks for the encouragement and the link! I'll definitely check out @tiangolo's tutorial to deepen my FastAPI security knowledge. Appreciate your support!

Best wishes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request reviewed
Projects
None yet
Development

No branches or pull requests