28 : Securing JWT Login with HttpOnly Cookie

Resources:

We have an api for login, which returns a JWT Token, and its working fine. But there is a problem when it comes to web apps. It was okay for apis but now if we don't modify our logic to store the jwt token. Our application would be vulnerable to several security attacks like XSS and CSRF. Take some time to go through OWASP Top Ten Web Application Security Risks | OWASP. We are currently using the OAuth2PasswordBearer, If you see its implementation  you will notice it tries to retrieve the token from a key named Authorization from the header of the request(Line 153). What I suggest is to modify the usage of this OAuth2PasswordBearer. So as to extract the token from an HttpOnly cookie🍪. HttpOnly cookies can't be accessed by javascript. So, any client-side malicious javascript would not be able to access the cookie data and our application with be more secure. Lets create a new file names apis > utils.py in which we will store the logic to extract token from HttpOnly cookie.

from fastapi.security import OAuth2
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi import Request
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import HTTPException
from fastapi import status
from typing import Optional
from typing import Dict


class OAuth2PasswordBearerWithCookie(OAuth2):
    def __init__(
        self,
        tokenUrl: str,
        scheme_name: Optional[str] = None,
        scopes: Optional[Dict[str, str]] = None,
        auto_error: bool = True,
    ):
        if not scopes:
            scopes = {}
        flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
        super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)

    async def __call__(self, request: Request) -> Optional[str]:
        authorization: str = request.cookies.get("access_token")  #changed to accept access token from httpOnly Cookie
        print("access_token is",authorization)

        scheme, param = get_authorization_scheme_param(authorization)
        if not authorization or scheme.lower() != "bearer":
            if self.auto_error:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Not authenticated",
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                return None
        return param

Now, Instead of using OAuth2PasswordBearer default implementation, we will use our own implementation of OAuth2PasswordBearer. Time to fix the logic for login api. Make the below changes to apis > version1 > route_login.py

# from fastapi.security import OAuth2PasswordBearer #no longer needed
from fastapi import Response    #new
from apis.utils import OAuth2PasswordBearerWithCookie    #new


@router.post("/token", response_model=Token)
def login_for_access_token(response: Response,form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):  #added response as a function parameter
    user = authenticate_user(form_data.username, form_data.password, db)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )
    access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.email}, expires_delta=access_token_expires
    )
    response.set_cookie(key="access_token",value=f"Bearer {access_token}", httponly=True)  #set HttpOnly cookie in response
    return {"access_token": access_token, "token_type": "bearer"}


oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/login/token")   #changed to use our implementation

Time to test our implementation. Head to Job Board - Swagger UI and try to make a login request and then see your cookies by right-clicking and inspecting the webpage.

Final git commit: Secure JWT token using HttpOnly Cookie · nofoobar/JobBoard-Fastapi@f00ffd9 (github.com)

Prev: 27 : User … Next: 29 : Implementing …