'Reading Cookie from React (backend with FastAPI + fastapi-jwt-auth)
I am having some problems with understanding JWT in Cookie using Axios and FastAPI.
I am trying to make a simple application with React for the frontend and FastAPI for the backend. Since this is more like my study project, I decided to use JWT for authentication and store them in Cookie.
The authentication flow is quite basic. User sends credentials to the backend via POST and backend will set the JWT to Cookie and send it back.
The issue I have is I cannot read the cookies returned to the frontend.
Now, I understand you cannot read HttpOnly cookie from Javascript, thus even if the cookies are set from the login request I cannot see them from my React application.
If I use Insomnia to check the behavior of the login API, it works just fine and sets the appropriate cookies.
However, the package I use for JWT authentication called fastapi-jwt-auth sends the CSRF token via Cookie. And this is NOT httpOnly.
So in order to get and use my CSRF token I need to read the Cookie from React. I thought I can do this because hey, the CSRF token isn't HttpOnly. But as you can see from the image set-cookie
does not exist in the headers.
I am assuming this is because cookies sent from the backend are a mix with HttpOnly cookie and not HttpOnly cookie. But I cannot confirm this from my research.
So how can I get this non HttpOnly cookie from the response? Or is this simply not possible?
Here is my sample project.
frontend
package.json
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.0",
"@types/jest": "^26.0.20",
"@types/node": "^12.20.4",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
"axios": "^0.21.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.3",
"typescript": "^4.2.2",
"web-vitals": "^1.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
src/App.tsx
import React, { useState } from "react";
import "./App.css";
import { authAPI } from "networking/api";
const App = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
const response = await authAPI.login({ email, password });
console.log(response);
const cookies = response.headers["set-cookie"];
console.log(cookies);
} catch (err) {
console.error(err);
}
};
return (
<div className="App">
<input
value={email}
onChange={(event) => setEmail(event.target.value)}
type="email"
/>
<input
value={password}
onChange={(event) => setPassword(event.target.value)}
type="password"
/>
<button type="submit" onClick={handleLogin}>
login
</button>
</div>
);
};
export default App;
src/networking/api.ts
import axios from "axios";
import { Auth } from "models/auth";
const client = axios.create({
baseURL: "http://localhost:8000/",
responseType: "json",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
const auth = () => {
return {
async login(credential: Auth) {
return await client.post("auth/login", credential);
},
};
};
const authAPI = auth();
export { authAPI };
export default client;
src/models/auth.ts
type Auth = {
email: string;
password: string;
};
export type { Auth };
backend
Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
fastapi = "*"
fastapi-jwt-auth = "*"
uvicorn = "*"
[dev-packages]
[requires]
python_version = "3.9"
main.py
import uuid
from fastapi import FastAPI, HTTPException, status, Body, Depends
from fastapi_jwt_auth import AuthJWT
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, BaseSettings
import uvicorn
app = FastAPI()
SECRET_KEY = '6qvsE3BBe7xvG4azL8Wwd3t_uVqXfFot6QRHIHZREkwrTZYnQHv6fSjInCB7'
class Settings(BaseSettings):
authjwt_secret_key: str = SECRET_KEY
authjwt_token_location: set = {'cookies'}
authjwt_cookie_secure: bool = True
authjwt_cookie_csrf_protect: bool = True
authjwt_cookie_samesite: str = 'lax'
@AuthJWT.load_config
def get_config():
return Settings()
CORS_ORIGINS = ["http://localhost:8080", "http://localhost:8000", "http://localhost:5000", "http://localhost:3000",]
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
class UserLoginIn(BaseModel):
email: str
password: str
@app.post('/auth/login')
async def login_user(user_in: UserLoginIn = Body(...), Authorize: AuthJWT = Depends()):
try:
user_id = str(uuid.uuid4())
# create tokens
access_token = Authorize.create_access_token(subject=user_id)
refresh_token = Authorize.create_refresh_token(subject=user_id)
# set cookies
Authorize.set_access_cookies(access_token)
Authorize.set_refresh_cookies(refresh_token)
except Exception as err:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Failed Authorization.')
return {'message': 'Successfully login'}
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
Thank you in advance for any insights :)
Solution 1:[1]
This looks like an issue caused by Chrome 80's recent update for cookies with a SameSite policy -- more info on this policy can be read here: https://www.troyhunt.com/promiscuous-cookies-and-their-impending-death-via-the-samesite-policy/
I've recreated your environment and it looks like we can easily get the cookies to show up on Chrome's Cookies Storage by setting
authjwt_cookie_samesite: str = 'none'
which will then default to 'Lax' within DevTools Network headers, which is Chrome's default policy (see the post I attached). When I set
authjwt_cookie_samesite: str = 'lax'
and checked Chrome's DevTools Network Headers, it looks like there was an error warning (b/c Chrome accepts these parameters with capitalization). image
This seems to be an unresolved issue with the library: https://github.com/IndominusByte/fastapi-jwt-auth/issues/79
Solution 2:[2]
For sessions you should use HttpOnly because its pretty much the same algorithm every time on JWT, CSRF cookie is readable by JS because it changes every time and you need the csrf-token to send the requests.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Shaikat Islam |
Solution 2 | Luiz Felipe Lima |