FastAPI is not returning cookies to React frontend
Asked Answered
B

1

5

Why doesn't FastAPI return the cookie to my frontend, which is a React app?

Here is my code:

@router.post("/login")
def user_login(response: Response,username :str = Form(),password :str = Form(),db: Session = Depends(get_db)):
    user = db.query(models.User).filter(models.User.mobile_number==username).first()
    if not user:
        raise HTTPException(400, detail='wrong phone number or password')
    if not verify_password(password, user.password):
        raise HTTPException(400, detail='wrong phone number or password')
    
   
    access_token = create_access_token(data={"sub": user.mobile_number})
    response.set_cookie(key="fakesession", value="fake-cookie-session-value") #here I am set cookie 
    return {"status":"success"}  

When I login from Swagger UI autodocs, I can see the cookie in the response headers using DevTools on Chrome browser. However, when I login from my React app, no cookie is returned. I am using axios to send the request like this:

await axios.post(login_url, formdata)

Bred answered 5/10, 2022 at 15:27 Comment(1)
What is the actual response? Is it 200 OK, or is there an error that occurs? What does the response headers look like?Crocoite
W
12

First, create the cookie, as shown in the example below, and make sure there is no error returned when performing the Axios POST request, and that you get a 'status': 'success' response with 200 status code. You may want to have a look at this answer as well, which provides explains how to use the max_age and expires flags too.

from fastapi import FastAPI, Response

app = FastAPI()

@app.get('/')
def main(response: Response):
    response.set_cookie(key='token', value='some-token-value', httponly=True) 
    return {'status': 'success'}

Second, as you mentioned that you are using React in the frontend—which needs to be listening on a different port from the one used for the FastAPI backend, meaning that you are performing CORS requests—you need to set the withCredentials property to true (by default this is set to false), in order to allow receiving/sending credentials, such as cookies and HTTP authentication headers, from/to other origins. Two servers with same domain and protocol, but different ports, e.g., http://localhost:8000 and http://localhost:3000 are considered different origins (see FastAPI documentation on CORS and this answer, which provides details around cookies in general, as well as solutions for setting cross-domain cookies—which you don't actually need in your case, as the domain is the same for both the backend and the frontend, and hence, setting the cookie as usual would work just fine).

Note that if you are accessing your React frontend by typing http://localhost:3000 in the address bar of your browser, then your Axios requests to FastAPI backend should use the localhost domain in the URL, e.g., axios.post('http://localhost:8000',..., and not axios.post('http://127.0.0.1:8000',..., as localhost and 127.0.0.1 are two different domains, and hence, the cookie would otherwise fail to be created for the localhost domain, as it would be created for 127.0.0.1, i.e., the domain used in the axios request (and then, that would be a case for cross-domain cookies, as described in the linked answer above, which again, in your case, would not be needed).

Thus, to accept cookies sent by the server, you need to use withCredentials: true in your Axios request; otherwise, the cookies will be ignored in the response (which is the default behaviour, when withCredentials is set to false; hence, preventing different domains from setting cookies for their own domain). The same withCredentials: true property has to be included in every subsequent request to your API, if you would like the cookie to be sent to the server, so that the user can be authenticated and provided access to protected routes.

Hence, an Axios request that includes credentials should look like this:

await axios.post(url, data, {withCredentials: true}))

The equivalent in a fetch() request (i.e., using Fetch API) is credentials: 'include'. The default value for credentials is same-origin. Using credentials: 'include' will cause the browser to include credentials in both same-origin and cross-origin requests, as well as set any cookies sent back in cross-origin responses. For instance:

fetch('https://example.com', {
  credentials: 'include'
});

Important Note

Since you are performing a cross-origin request, for either the above to work, you would need to explicitly specify the allowed origins, as described in this answer (behind the scenes, that is setting the Access-Control-Allow-Origin response header). For instance:

origins = ['http://localhost:3000', 'http://127.0.0.1:3000',
           'https://localhost:3000', 'https://127.0.0.1:3000'] 

Using the * wildcard instead would mean that all origins are allowed; however, that would also only allow certain types of communication, excluding everything that involves credentials, such as cookies, authorization headers, etc—hence, you should not use the * wildcard.

Also, make sure to set allow_credentials=True when using the CORSMiddleware (which sets the Access-Control-Allow-Credentials response header to true).

Example (see here):

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
Wirra answered 5/10, 2022 at 17:3 Comment(5)
Also make sure to set your Cookie's SameSite to none (lax did not work for me), see this answer for more https://mcmap.net/q/49412/-reading-cookie-from-react-backend-with-fastapi-fastapi-jwt-authRepartee
@Repartee Please DON'T set the SameSite flag to None, as the cookie would not be protected from external access (use with cross-domain cookies only).Wirra
@Repartee If you are creating cookies for the same domain (e.g., http://localhost or http://127.0.0.1), setting that flag to Lax or Strict should be fine. Note that localhost and 127.0.0.1 are considered to be different domains (the same applies to https://localhost and http://localhost - note the s in the first domain, which uses the HTTPS protocol); hence, if you are accessing your frontend at http://localhost, make sure that your axios request uses http://localhost, not http://127.0.0.1, and vice versa.Wirra
for development it's fine to set your cookies to None, for production however, you are totally right to not set it to None.Repartee
@Repartee I am afraid that's not how it works. I would suggest you have a look at the answer above (including the references), as well as the comments above to better understand how things work, and that in the above case (where same-domain cookies need to be created), you don't have to and shouldn't set the SameSite flag to None. Making that mistake during development, it would be easier for you to make it in production as well. Read more about cross-site and same-site cookies here.Wirra

© 2022 - 2024 — McMap. All rights reserved.