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

enter image description here

However, the package I use for JWT authentication called fastapi-jwt-auth sends the CSRF token via Cookie. And this is NOT httpOnly.

enter image description here

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.

enter image description here

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.

How to read a HttpOnly cookie using JavaScript

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