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)
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.
Note that two servers with same domain and protocol, but different port numbers , e.g., http://localhost:8000
and http://localhost:3000
are considered different origins (see FastAPI documentation on CORS and details later on how to enable CORS in your FastAPI backend in such cases). You may also have a look at this answer, which provides details around HTTP cookies in general, as well as solutions for setting cross-site cookies—which you don't actually need in your case, as the domain name is the same for both the backend and frontend (regardless of the port numbers that differ, as cookies do not provide isolation by port), and hence, setting the cookie as usual should work just fine.
As you may have already understood, there is a distinction between same-site
/cross-site
and same-origin
/cross-origin
requests. In summary, two URLs are considered to be same-site
, if they have the same scheme (e.g., http
or https
) and the same domain (e.g., localhost
, 127.0.0.1
or example.com
). They don't need to have the same port or subdomain. On the other hand, two URLs are considered to be same-origin
, if they have the same scheme, domain, subdomain and port number as well.
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, for instance:
axios.post('http://localhost:8000',...
and not 127.0.0.1
, such as:
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-site cookies, as described in the linked answer above, which again, in your case, would not be needed). Similarly if you are accessing the frontend page in your browser at http://127.0.0.1:3000
, your JS requests should use 127.0.0.1
; for instance, axios.post('http://127.0.0.1:8000',...
.
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 by cross-origin responses. For instance:
fetch('https://example.com', {
credentials: 'include'
});
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=["*"],
)