A real-life example of how cookies are used in the FastAPI web framework.
0FastAPI is a relatively new web framework, with high growth potential — according to surveys and reports that show how many big companies (like Uber and Netflix) are using it!
Cookies are small pieces of data stored on the client's browser by the web server. They play a crucial role in maintaining user sessions, tracking user preferences, and enhancing security.
Here we will learn how to use cookies in a good way in FastAPI!
Ʈhinga is a web application that lets users compare and rank various items. By presenting two items at a time, users can vote for their favorite, contributing to a dynamic ranking system. It serves as a fun way to gauge popularity and user preferences.
I don't mention the whole project code, just the sections that can be important are written...
Using enums like SessionStatus
ensures clarity and consistency in the code, making it easier to manage different states of user sessions.
class SessionStatus(StrEnum):
ACTIVE = auto()
INACTIVE = auto()
EXPIRED = auto()
For modeling, I like to use SQLAlchemy here, but SQLModel and Tortoise ORM are another alternatives...
class Session(Base):
__tablename__ = "sessions"
id = Column(Integer, primary_key=True, index=True)
access_token = Column(
String(32),
default=lambda: uuid.uuid4().hex,
unique=True,
index=True,
)
client_fingerprint = Column(String(64), nullable=False)
status = Column(
Enum(enums.SessionStatus),
default=enums.SessionStatus.ACTIVE,
)
expires_at = Column(
DateTime,
default=lambda: (
datetime.now(timezone.utc) + timedelta(days=SESSION_EXPIRE_DAYS)
),
)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
user = relationship("User", back_populates="sessions")
This model defines the structure of the sessions table, including the session status managed by the SessionStatus
enum.
Explanation about some fields of the sessions table:
access_token
: A unique token generated for each session, defaults to a randomly generated UUID, and is both unique and indexed.client_fingerprint
: A unique identifier for the client's browser, which helps in preventing session hijacking by tracking the client's device and browser characteristics. While it is not foolproof, it adds an extra layer of security to mitigate session hijacking attempts.expires_at
: Specifies the expiration date and time of the session, calculated by adding a specified number of days (defined by SESSION_EXPIRE_DAYS
) to the current UTC time. This ensures that sessions do not persist indefinitely.Here are some CRUD operations:
def get_session_by_access_token(
*,
db: Session,
access_token: str,
) -> Optional[models.Session]:
return (
db.query(models.Session)
.filter(models.Session.access_token == access_token)
.first()
)
def create_session(
*,
db: Session,
user_id: int,
client_fingerprint: str,
) -> models.Session:
db_session = models.Session(
client_fingerprint=client_fingerprint,
user_id=user_id,
)
db.add(db_session)
db.commit()
db.refresh(db_session)
return db_session
def deactivate_session(*, db: Session, access_token: str) -> None:
db_session = get_session_by_access_token(db=db, access_token=access_token)
if db_session is not None:
db_session.status = enums.SessionStatus.INACTIVE
db.commit()
def verify_session(
*,
db: Session,
access_token: str,
client_fingerprint: str,
) -> Optional[models.Session]:
db_session = get_session_by_access_token(db=db, access_token=access_token)
if (
db_session is None
or db_session.status
in (
enums.SessionStatus.INACTIVE,
enums.SessionStatus.EXPIRED,
)
or db_session.client_fingerprint != client_fingerprint
):
return None
elif db_session.expires_at.replace(tzinfo=timezone.utc) <= datetime.now(
timezone.utc
):
db_session.status = enums.SessionStatus.EXPIRED
db.commit()
return db_session
I prefer to define my CRUD functions using keyword-only arguments, which enhances readability and clarity — this is the first project where I have implemented this approach. :)
Note: Some ORM libraries allow you to define helper functions directly within the class to manage table fields, enhancing the encapsulation and organization of your database logic.
Look at environment variables and configuration constants first:
DEBUG_ENABLED = os.environ["DEBUG_ENABLED"] == "1"
ALLOWED_ORIGINS = re.split(r"[,;]\s?", os.environ["ALLOWED_ORIGINS"])
COOKIE_SECURE_MODE = not DEBUG_ENABLED
COOKIE_NO_JS_ACCESS = os.environ["COOKIE_NO_JS_ACCESS"] == "1"
COOKIE_SAMESITE_POLICY = "lax" if DEBUG_ENABLED else "none"
Related dependency functions:
async def get_access_token(request: Request) -> str:
access_token = request.cookies.get("access_token")
if access_token is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated.",
)
return access_token
async def get_current_user(
request: Request,
access_token: str = Depends(get_access_token),
db: Session = Depends(get_db),
) -> models.User:
client_fingerprint = utils.generate_client_fingerprint(request)
db_session = crud.verify_session(
db=db,
access_token=access_token,
client_fingerprint=client_fingerprint,
)
if db_session is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated.",
)
db_user = crud.get_user_by_id(db=db, user_id=db_session.user_id)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found.",
)
return db_user
Authentication endpoints:
router = APIRouter()
@router.post("/login/", response_model=schemas.User)
async def login(
request: Request,
response: Response,
username: str = Body(...),
password: str = Body(...),
db: Session = Depends(get_db),
):
db_user = crud.get_user_by_username(db=db, username=username)
if db_user is None or not utils.verify_password(
password, db_user.hashed_password
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password.",
)
client_fingerprint = utils.generate_client_fingerprint(request)
db_session = crud.create_session(
db=db,
user_id=db_user.id,
client_fingerprint=client_fingerprint,
)
response.set_cookie(
key="access_token",
value=db_session.access_token,
httponly=COOKIE_NO_JS_ACCESS,
secure=COOKIE_SECURE_MODE,
samesite=COOKIE_SAMESITE_POLICY,
)
return db_user
@router.post("/logout/")
async def logout(
response: Response,
access_token: str = Depends(get_access_token),
db: Session = Depends(get_db),
):
crud.deactivate_session(db=db, access_token=access_token)
response.delete_cookie(key="access_token")
return {"message": "Successfully logged out."}
@router.get("/users/me/", response_model=schemas.User)
async def read_users_me(
current_user: models.User = Depends(get_current_user),
):
return current_user
Cookie security attributes:
httponly
: This flag ensures that the cookie is inaccessible to JavaScript, reducing the risk of XSS attacks.secure
: This flag ensures that the cookie is transmitted over HTTPS, which is crucial for securing sensitive data.samesite
: This attribute helps protect against CSRF attacks. The lax
value allows the cookie to be sent with requests initiated by third-party websites, but only if the request is a GET request.To handle cookies correctly, especially in a cross-origin scenario, you need to configure CORS properly:
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True,
)
By following these practices, you can securely and effectively manage cookies in your FastAPI application.
Our website index page:
Screenshots of the login and registration forms, as well as the user profile dashboard:
The user interface of the comparison and voting game:
Top 20 things leaderboard based on votes:
You can now contribute to the project by visiting our repositories: back-end and front-end. The project is open to contributions and feedback! ;)
Useful links: