Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Cookie based JWT tokens #480

Closed
joaodlf opened this issue Aug 29, 2019 · 19 comments
Closed

Cookie based JWT tokens #480

joaodlf opened this issue Aug 29, 2019 · 19 comments
Labels
question Question or problem question-migrate

Comments

@joaodlf
Copy link

joaodlf commented Aug 29, 2019

Description
First of all, I want to thank you for FastAPI - It's has been a while since I have been this excited about programming for the web. FastAPI is, so far, a really interesting project.

Looking through the documentation, I can see a very clear and concise practical guide to implement JWT tokens. I can see that the access token is returned as part of the response body:

@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user_dict = fake_users_db.get(form_data.username)
    if not user_dict:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    user = UserInDB(**user_dict)
    hashed_password = fake_hash_password(form_data.password)
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}

This appears to be a requirement for the /docs to work as expected (where one can login and execute calls on the fly), this is really cool functionality, but it seems to be tied down to the response body.

I would like to be able to set a secure and httpOnly cookie to hold the access token, as I feel that exposing the access token as part of the response body is detrimental to the security of my application. At the same time, I would like the /docs to remain functional with a cookie based approach.

Would this be straightforward to accomplish? Is this at all supported out of the box by FastAPI?

@joaodlf joaodlf added the question Question or problem label Aug 29, 2019
@joaodlf
Copy link
Author

joaodlf commented Aug 29, 2019

So I actually got this to work with some simple changes, here is a small writeup for anyone that might be interested:

First thing is to update the /token route:

@router.post("/token", tags=["auth"])
async def auth_token(response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
    
    # auth logic here...

    access_token = create_access_token(
        data={"sub": username}, expires_delta=access_token_expires
    ).decode("utf-8")

    response.set_cookie(key="access_token",value=f"Bearer {access_token}", httponly=True)

    return

Notice that I need to .decode("utf-8") the access token (else you run into Invalid header padding errors when decoding the jwt). I also set my httponly cookie and return an empty response.

Now we need to create a similar class to OAuth2PasswordBearer, this is the class FastAPI needs to find the token url:

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")

So this is the new class we need to replace it with:

class OAuth2PasswordBearerWithCookie(OAuth2):
    def __init__(
        self,
        tokenUrl: str,
        scheme_name: str = None,
        scopes: dict = None,
        auto_error: bool = True,
    ):
        if not scopes:
            scopes = {}
        flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
        super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)

    async def __call__(self, request: Request) -> Optional[str]:
        authorization: str = request.cookies.get("access_token")

        scheme, param = get_authorization_scheme_param(authorization)
        if not authorization or scheme.lower() != "bearer":
            if self.auto_error:
                raise HTTPException(
                    status_code=HTTP_401_UNAUTHORIZED,
                    detail="Not authenticated",
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                return None

        return param

This is the same exact code for OAuth2PasswordBearer with a small modification: authorization: str = request.cookies.get("access_token") - Instead of grabbing the token from the Authentication header, we get it from the access_token cookie.

Your code now becomes:

oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/auth/token")

This works with /docs as well since the browser always sends back httonly cookies!

@wshayes
Copy link
Contributor

wshayes commented Aug 29, 2019

FYI - Blog series using this approach with FastAPI by Nils de Bruin - https://medium.com/@nils_29588

@andre-dasilva
Copy link

andre-dasilva commented May 2, 2020

In the future will this method be included in the docs? I think a httponly cookie is the best way to store the jwt token, and not sending in the body with json. Also thanks @joaodlf i will try your solution in my project in the next couple of days.

Edit: Tried it today and it works nicely.

@bo5o
Copy link

bo5o commented May 7, 2020

Isn't this approach still vulnerable to CSRF attacks?
Adding the SameSite=Lax flag to the cookie could mitigate most of the vulnerabilities that come with cookies (and works in most modern browsers). Otherwise you would have to add some kind of verification cookie to your frontend and implement additional logic to handle this (similar to Django csrf_token).

@andre-dasilva
Copy link

andre-dasilva commented May 8, 2020

@cbows yes it is still vulnerable to CSRF. But it's XSS safe (with httponly=true), which sending in the body (and storing on the client) isn't. SameSite is not fully supported in all browsers https://caniuse.com/#search=samesite yet. But i do think as well, that it does minimize the vulnerability a lot, if you don't have some cross-origin scenarios.

Otherwise yes you have to use a token. in fastapi you could maybe implement it in your jwt claim and store it on the client. and with every request you send it in the header and compare it with the claim

I think stuff like this would be awesome to include in the docs. fastapi and also the docs are really awesome. Thats why i think to have the best possible security in the docs is really nice and a big selling point :-)

@bo5o
Copy link

bo5o commented May 8, 2020

I totally agree with you. This definitely should go in the docs. With SameSite=Lax it might even be superior to the current approach in terms of default security.

@bo5o
Copy link

bo5o commented May 10, 2020

According to starlette docs, samesite='lax' is the default when setting cookies.

@andre-dasilva
Copy link

I have to look if it's beeing set in my cookie. But that would make it even more relevant to include this in the docs or even implement a class in fastapi

@andre-dasilva
Copy link

andre-dasilva commented May 11, 2020

@cbows I just checked my cookie and it does not set samesite. current version of fastapi (0.54.1) uses starlette (0.13.2). If you check starlette the current version is 0.13.4 and there it is set in the code. so fastapi first needs to update to 0.13.4 as a dependency. right now we have to set it manually

@adamerose
Copy link

I agree the docs should be updated to an example with HttpOnly cookies since I think that's best practice to protect session tokens from XSS

@SelfhostedPro
Copy link

Just as a heads up, you could follow the way that flask_jwt_extended does things where the function that sets the tokens as cookies also creates csrf tokens:
https://flask-jwt-extended.readthedocs.io/en/stable/tokens_in_cookies/

@F1r3Hydr4nt
Copy link

F1r3Hydr4nt commented Sep 2, 2020

FYI, @wshayes broken link (I think) should lead to this: https://archive.is/UsaXo

I agree the docs should be updated to an example with HttpOnly cookies since I think that's best practice to protect session tokens from XSS

Perhaps @SelfhostedPro's suggestion which shows:

NOTE: This is just a basic example of how to enable cookies. This is
vulnerable to CSRF attacks, and should not be used as is. See
csrf_protection_with_cookies.py for a more complete example!

And leads to: https://archive.is/Bv1ub
Could be incorporated for a fully secure solution whilst maintaining old browser support

@encryptblockr
Copy link

what is latest on this?

@encryptblockr
Copy link

FYI - Blog series using this approach with FastAPI by Nils de Bruin - https://medium.com/@nils_29588

this page is now 404

@ieferrari
Copy link

I think a better option is to save the token in a samtesite=Strict cookie

from owasp csrf-cheatsheet:

The Strict value will prevent the cookie from being sent by the browser to the target site in all cross-site browsing context, even when following a regular link.

afaik this will prevent csrf and xss attacks

@ghost
Copy link

ghost commented Aug 20, 2021

Would it be an alternative to stick with a short-lived access-token in the body and add an additional long-lived refresh-token in a cookie?

@F1r3Hydr4nt
Copy link

FYI - Blog series using this approach with FastAPI by Nils de Bruin - https://medium.com/@nils_29588

this page is now 404

https://archive.is/UsaXo

@BigSamu
Copy link

BigSamu commented Mar 31, 2022

[FEATURE REQUEST] Authentication to use HttpOnly Cookie instead of Local Storage tokens Buuntu/fastapi-react#165

Hi Joao,

Many thanks for this code. It is working perfectly with my API. However, I wonder about one important issue. Whenever you try to log out using the interface from swagger (screenshot below), the cookie still keeps in the browser letting to access the secured endpoints you have.

image

Consider these two cases below. In the first case, you can see that the endpoint "/api/v1/users/me" should not allow access because the user is not yet authenticated (this can be checked with the lock open). However because the cookie is still in the browser, you can retrieve the data from the endpoint anyway

image

In this second case, I delete the cookie manually from my browser. After doing that, the behaviour is the one expected (if you try to get data from the endpoint when you are not authenticated you should receive a 401 response, Unauthorized)

image

Wonder how this can be fixed in terms of whenever you logout using the Login/Logout Form you can add/destroy the cookie. Any ideas?

@joaodlf
Copy link
Author

joaodlf commented Apr 4, 2022

Hi @BigSamu, it's been quite a while since I've touched a project that uses fastapi, but your conclusion at the end makes sense to me - You'll need to hook into the logout endpoint (possibly via class extension as well?) and delete the cookie.

@tiangolo tiangolo changed the title [QUESTION] Cookie based JWT tokens Cookie based JWT tokens Feb 24, 2023
@fastapi fastapi locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #9142 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests