Message!
This is an old work, for updated version please visit Updated FastAPI WebApp Course
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)
Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.
We know, we might make it hard for you but definitely worth the efforts.
© Copyright 2022-23 Team FastAPITutorial