From fb7284b117c96962b99001d40b786c1317e83657 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Mon, 27 Apr 2026 09:44:06 +0200 Subject: [PATCH] feat: add JWT creation, decoding and cookie helpers --- app/core/auth.py | 47 +++++++++++++++++++++++++++++++++++++++++ tests/test_core_auth.py | 27 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 app/core/auth.py create mode 100644 tests/test_core_auth.py diff --git a/app/core/auth.py b/app/core/auth.py new file mode 100644 index 0000000..8b14fab --- /dev/null +++ b/app/core/auth.py @@ -0,0 +1,47 @@ +from datetime import datetime, timedelta, timezone +from typing import Optional + +from fastapi import Request, Response +from jose import JWTError, jwt + +from app.core.config import get_settings + +settings = get_settings() + +COOKIE_NAME = "access_token" +ALGORITHM = "HS256" +TOKEN_EXPIRE_HOURS = 8 + + +def create_access_token(username: str, is_admin: bool) -> str: + expire = datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRE_HOURS) + return jwt.encode( + {"sub": username, "is_admin": is_admin, "exp": expire}, + settings.SECRET_KEY, + algorithm=ALGORITHM, + ) + + +def decode_token(token: str) -> Optional[dict]: + try: + return jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) + except JWTError: + return None + + +def get_token_from_request(request: Request) -> Optional[str]: + return request.cookies.get(COOKIE_NAME) + + +def set_auth_cookie(response: Response, token: str) -> None: + response.set_cookie( + key=COOKIE_NAME, + value=token, + httponly=True, + samesite="lax", + secure=settings.APP_ENV == "production", + ) + + +def clear_auth_cookie(response: Response) -> None: + response.delete_cookie(key=COOKIE_NAME, httponly=True, samesite="lax") diff --git a/tests/test_core_auth.py b/tests/test_core_auth.py new file mode 100644 index 0000000..f4a819e --- /dev/null +++ b/tests/test_core_auth.py @@ -0,0 +1,27 @@ +from app.core.auth import COOKIE_NAME, create_access_token, decode_token + + +def test_create_and_decode_token(): + token = create_access_token(username="alice", is_admin=False) + payload = decode_token(token) + assert payload is not None + assert payload["sub"] == "alice" + assert payload["is_admin"] is False + + +def test_admin_claim(): + token = create_access_token(username="admin", is_admin=True) + assert decode_token(token)["is_admin"] is True + + +def test_decode_invalid_token(): + assert decode_token("not.a.valid.token") is None + + +def test_decode_tampered_token(): + token = create_access_token(username="alice", is_admin=False) + assert decode_token(token[:-4] + "xxxx") is None + + +def test_cookie_name(): + assert COOKIE_NAME == "access_token"