Search code examples
oauth-2.0jwtfetchfastapicookie-httponly

How do I validate a JWT that's sent as an HttpOnly cookie in FastAPI?


Problem

I'm working on a FastAPI application that requires authentication for certain endpoints to be reached by users. I'm using Oauth2 and Jose from FastAPI to create JWTs for my authentication process. After doing some research, it seems that the best way to ensure tokens are protected on the frontend is to store them in HttpOnly Cookies. I am struggling to understand how to CORRECTLY pass the JWT through HttpOnly Cookies so that my FastAPI server is able to validate the JWT in my headers. Currently, when I try to pass the JWT token as an HttpOnly Cookie, I get a 401 Unauthorized Error.

What I've Tried

I have been able to successfully authenticate the user with the JWT token when I code the token into the headers as a template string. However, when I pass the JWT to the FastAPI server through the headers as a Cookie, my FastAPI server is unable to authenticate the user and returns a 401 unauthorized error. I've tried looking into the network tab to see what headers are being sent in my requests to the FastApi server in order to better understand what is different between the two scenarios.

Successful example with code

This is in the header when I pass the JWT as a template string and get a 200 response:

Authentication: Bearer token

  async function getPosts() {
    const url = "http://localhost:8000/posts";
    const fetchConfig = {
      headers: {
        Authorization: `Bearer ${tokenValue}`,
      },
    };
    const response = await fetch(url, fetchConfig);
    const posts = await response.json();
  }

Unsuccessful example with code

This is in the header when I pass the JWT as an HttpOnly Cookie and get a 401 response:

Cookie: access_token="Bearer token"

I've also tried changing the way I set my cookie on the server so that the header looks like this:

Cookie: Authentication="Bearer token"

  async function getPosts() {
    const url = "http://localhost:8000/posts";
    const fetchConfig = {
      credentials: "include",
    };
    const response = await fetch(url, fetchConfig);
    const posts = await response.json();
    console.log(posts);
  }

FastAPI Code

Here is the code for my Oauth2 token validation that protects my API endpoints. This is based off of the example in the FastAPI docs: FastApi Oauth2

oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login')

SECRET_KEY = settings.SECRET_KEY
ALGORITHM = settings.ALGORITHM
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES


def verify_access_token(token: str, credentials_exception):
  try:
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    id: str = payload.get("user_id")
    if id is None:
      raise credentials_exception
    token_data = schemas.TokenData(id=id)
  
  except JWTError:
    raise credentials_exception

  return token_data

def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):
  credentials_exception = HTTPException(
    status_code=status.HTTP_401_UNAUTHORIZED,
    detail=f"Could not validate credentials",
    headers={"WWW-Authenticate": "Bearer"}
  )

  token = verify_access_token(token, credentials_exception)
  user = db.query(models.User).filter(models.User.id == token.id).first()

  return user

Here is an example of a protected endpoint that depends on the get_current_user function from the oauth2 file listed above.

@router.get("/", response_model=List[schemas.PostOut])
def get_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user):
  return {"Message": "Protected Endpoint Reached"}

It seems like I'm running into the issue because my get_current_user function in the Oauth2 is only able to grab the JWT from the header when it is in the following format:

Authentication: Bearer token

It doesn't seem to be able to authenticate the token from the header when it is in either of the following formats:

Cookie: access_token="Bearer token"

Cookie: Authentication="Bearer token"

Do I need to somehow change the way I'm sending the headers when I send them via HttpOnly Cookies or do I maybe need to change something about my get_current_user function that will enable it to read the cookie headers correctly.

Any suggestions are greatly appreciated, and thank you for taking the time to read this!


Solution

  • To get the token from a cookie instead of the Authorization header which is default for OAuth2PasswordBearer, tell FastAPI that you want the token to originate from a cookie instead.

    def get_current_user(access_token: str = Cookie(...), db: Session = Depends(database.get_db)):
    

    This assume that the token has been named access_token (and not just token). Adjust the name as necessary.