refactor(backend): replace python-jose and passlib with PyJWT and bcrypt for security and simplicity

- Migrated JWT token handling from `python-jose` to `PyJWT`, reducing dependencies and improving error clarity.
- Replaced `passlib` bcrypt integration with direct `bcrypt` usage for password hashing.
- Updated `Makefile`, removing unused CVE ignore based on the replaced dependencies.
- Reflected changes in `ARCHITECTURE.md` and adjusted function headers in `auth.py`.
- Cleaned up `uv.lock` and `pyproject.toml` to remove unused dependencies (`ecdsa`, `rsa`, etc.) and add `PyJWT`.
- Refactored tests and services to align with the updated libraries (`PyJWT` error handling, decoding, and validation).
This commit is contained in:
2026-03-01 14:02:04 +01:00
parent 0553a1fc53
commit 1a36907f10
9 changed files with 84 additions and 139 deletions

View File

@@ -25,8 +25,8 @@ from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import UUID
from jose import JWTError, jwt
from jose.exceptions import ExpiredSignatureError
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
@@ -704,7 +704,7 @@ async def revoke_token(
"Revoked refresh token via access token JTI %s...", jti[:8]
)
return True
except JWTError:
except InvalidTokenError:
pass
except Exception: # noqa: S110 - Intentional: invalid JWT not an error
pass
@@ -827,7 +827,7 @@ async def introspect_token(
}
except ExpiredSignatureError:
return {"active": False}
except JWTError:
except InvalidTokenError:
pass
except Exception: # noqa: S110 - Intentional: invalid JWT falls through to refresh token check
pass

View File

@@ -537,8 +537,9 @@ class OAuthService:
AuthenticationError: If verification fails
"""
import httpx
from jose import jwt as jose_jwt
from jose.exceptions import JWTError
import jwt as pyjwt
from jwt.algorithms import RSAAlgorithm
from jwt.exceptions import InvalidTokenError
try:
# Fetch Google's public keys (JWKS)
@@ -552,24 +553,27 @@ class OAuthService:
jwks = jwks_response.json()
# Get the key ID from the token header
unverified_header = jose_jwt.get_unverified_header(id_token)
unverified_header = pyjwt.get_unverified_header(id_token)
kid = unverified_header.get("kid")
if not kid:
raise AuthenticationError("ID token missing key ID (kid)")
# Find the matching public key
public_key = None
jwk_data = None
for key in jwks.get("keys", []):
if key.get("kid") == kid:
public_key = key
jwk_data = key
break
if not public_key:
if not jwk_data:
raise AuthenticationError("ID token signed with unknown key")
# Convert JWK to a public key object for PyJWT
public_key = RSAAlgorithm.from_jwk(jwk_data)
# Verify the token signature and decode claims
# jose library will verify signature against the JWK
payload = jose_jwt.decode(
# PyJWT will verify signature against the RSA public key
payload = pyjwt.decode(
id_token,
public_key,
algorithms=["RS256"], # Google uses RS256
@@ -597,7 +601,7 @@ class OAuthService:
logger.debug("Google ID token verified successfully")
return payload
except JWTError as e:
except InvalidTokenError as e:
logger.warning("Google ID token verification failed: %s", e)
raise AuthenticationError("Invalid ID token signature")
except httpx.HTTPError as e: