2

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 :)

dropscar
  • 113
  • 2
  • 10
  • if you want to test your code you can disable HttpOnly flag and read the cookie values..but this should be your local testing only for security point of view. – MukulSharma Mar 02 '21 at 14:14
  • @MukulSharma Well in that case I am sure I can read the Cookie, but then I cannot use this in production – dropscar Mar 03 '21 at 02:03
  • please read this - https://stackoverflow.com/a/8069697/6426569 – MukulSharma Mar 03 '21 at 09:36
  • @dropscar did you end up solving this issue? mind posting solution as answer for others to use as help? thanks – uberrebu Jul 27 '21 at 07:11
  • @uberrebu hello! no, I ended up passing the CSRF token through a login success response. So if the user logs in the API returns the CSRF token to the client. Then set that to axios's header. Hope that helps! – dropscar Jul 28 '21 at 08:12
  • hmm not clear...mind pasting as answer? might help others besides me – uberrebu Jul 28 '21 at 08:16
  • @uberrebu unfortunately, I switched to Django with Django REST Framework to achieve this, so I don't think it is appropriate to answer this question.... – dropscar Jul 30 '21 at 16:31
  • wow...why did you do that? django REST over fastapi??? – uberrebu Jul 30 '21 at 17:16
  • @uberrebu for me, overall DRF is much easier to make APIs. Class Based Views were cleaner to write and read, Django's admin site is great, way more packages to use, etc. FastAPI is great. Easy to make APIs. But in my opinion, it gets messy very quickly. And the amount I needed to research and writing tests were not so "Fast". As for now Django + DRF was much easier. – dropscar Jul 31 '21 at 03:01
  • gotcha...i guess you wanted an opinionated framework and ok with django...for ppl that use fastapi..we want fully custom non-opinionated API design with nothing we dont need...anyways good luck with the project – uberrebu Jul 31 '21 at 07:29

2 Answers2

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

0

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