security.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. """
  2. Security utilities: JWT tokens, password hashing.
  3. """
  4. from datetime import datetime, timedelta, timezone
  5. from typing import Any
  6. from jose import JWTError, jwt
  7. from passlib.context import CryptContext
  8. from app.config import settings
  9. # Password hashing context
  10. pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
  11. def create_access_token(data: dict[str, Any]) -> str:
  12. """
  13. Create JWT access token.
  14. Args:
  15. data: Payload to encode (typically {"sub": user_id})
  16. Returns:
  17. Encoded JWT token string
  18. """
  19. to_encode = data.copy()
  20. expire = datetime.now(timezone.utc) + timedelta(
  21. minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
  22. )
  23. to_encode.update({"exp": expire, "type": "access"})
  24. return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
  25. def create_refresh_token(data: dict[str, Any]) -> str:
  26. """
  27. Create JWT refresh token.
  28. Args:
  29. data: Payload to encode (typically {"sub": user_id})
  30. Returns:
  31. Encoded JWT token string
  32. """
  33. to_encode = data.copy()
  34. expire = datetime.now(timezone.utc) + timedelta(
  35. days=settings.REFRESH_TOKEN_EXPIRE_DAYS
  36. )
  37. to_encode.update({"exp": expire, "type": "refresh"})
  38. return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
  39. def verify_token(token: str, expected_type: str = "access") -> dict[str, Any] | None:
  40. """
  41. Verify and decode JWT token.
  42. Args:
  43. token: JWT token string
  44. expected_type: Expected token type ("access" or "refresh")
  45. Returns:
  46. Decoded payload if valid, None otherwise
  47. """
  48. try:
  49. payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
  50. token_type: str = payload.get("type")
  51. if token_type != expected_type:
  52. return None
  53. return payload
  54. except JWTError:
  55. return None
  56. def verify_password(plain_password: str, hashed_password: str) -> bool:
  57. """
  58. Verify a plain password against a hashed password.
  59. Args:
  60. plain_password: Plain text password
  61. hashed_password: Hashed password from database
  62. Returns:
  63. True if password matches, False otherwise
  64. """
  65. return pwd_context.verify(plain_password, hashed_password)
  66. def hash_password(password: str) -> str:
  67. """
  68. Hash a password using bcrypt.
  69. Args:
  70. password: Plain text password
  71. Returns:
  72. Hashed password
  73. """
  74. return pwd_context.hash(password)