basyx commited on
Commit
ca5db6c
·
verified ·
1 Parent(s): 9cb2728

Update auth/security.py

Browse files
Files changed (1) hide show
  1. auth/security.py +30 -51
auth/security.py CHANGED
@@ -3,55 +3,52 @@ from datetime import datetime, timedelta, timezone
3
  from typing import Optional, Dict, Any
4
 
5
  from jose import jwt, JWTError
6
- from passlib.context import CryptContext
7
  from fastapi import HTTPException, status, Depends
8
  from fastapi.security import OAuth2PasswordBearer
9
 
 
 
10
 
11
  # =========================================================
12
- # CONFIG (ENV-FIRST, NO HARDCODED SECRETS)
13
  # =========================================================
14
 
15
- SECRET_KEY = os.getenv("SECRET_KEY", "")
16
  ALGORITHM = os.getenv("ALGORITHM", "HS256")
17
- ACCESS_TOKEN_EXPIRE_MINUTES = int(
18
- os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440")
19
- )
20
 
21
  if not SECRET_KEY:
22
  raise RuntimeError("SECRET_KEY is not set in environment variables")
23
 
24
 
25
  # =========================================================
26
- # PASSWORD HASHING (FIXED BCRYPT ISSUE)
27
  # =========================================================
28
- # IMPORTANT:
29
- # bcrypt_sha256 avoids:
30
- # - 72-byte password limit crash
31
- # - broken bcrypt backend detection (__about__ error)
32
 
33
  pwd_context = CryptContext(
34
- schemes=["bcrypt_sha256"],
35
  deprecated="auto",
 
 
 
36
  )
37
 
38
 
39
  def hash_password(password: str) -> str:
40
- """
41
- Enterprise-safe password hashing.
42
- - avoids bcrypt 72-byte limitation
43
- - avoids broken bcrypt backend in passlib
44
- """
45
- if not isinstance(password, str) or not password:
46
- raise ValueError("Password must be a non-empty string")
47
 
48
  return pwd_context.hash(password)
49
 
50
 
51
  def verify_password(plain_password: str, hashed_password: str) -> bool:
52
- """
53
- Constant-time password verification.
54
- """
55
  try:
56
  return pwd_context.verify(plain_password, hashed_password)
57
  except Exception:
@@ -59,7 +56,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
59
 
60
 
61
  # =========================================================
62
- # JWT TOKEN HANDLING
63
  # =========================================================
64
 
65
  def create_access_token(
@@ -70,36 +67,21 @@ def create_access_token(
70
  to_encode = data.copy()
71
 
72
  expire = datetime.now(timezone.utc) + (
73
- expires_delta
74
- if expires_delta
75
- else timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
76
  )
77
 
78
  to_encode.update({"exp": expire})
79
 
80
- encoded_jwt = jwt.encode(
81
- to_encode,
82
- SECRET_KEY,
83
- algorithm=ALGORITHM
84
- )
85
-
86
- return encoded_jwt
87
 
88
 
89
  def decode_token(token: str) -> Dict[str, Any]:
90
- """
91
- Decodes JWT and validates signature.
92
- """
93
  try:
94
- return jwt.decode(
95
- token,
96
- SECRET_KEY,
97
- algorithms=[ALGORITHM]
98
- )
99
  except JWTError:
100
  raise HTTPException(
101
  status_code=status.HTTP_401_UNAUTHORIZED,
102
- detail="Invalid authentication token",
103
  headers={"WWW-Authenticate": "Bearer"},
104
  )
105
 
@@ -112,12 +94,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
112
 
113
 
114
  def get_current_user(token: str = Depends(oauth2_scheme)) -> Dict[str, Any]:
115
- """
116
- Extracts authenticated user from JWT.
117
- """
118
  payload = decode_token(token)
119
 
120
- user_id: Optional[str] = payload.get("sub")
121
 
122
  if not user_id:
123
  raise HTTPException(
@@ -129,15 +108,15 @@ def get_current_user(token: str = Depends(oauth2_scheme)) -> Dict[str, Any]:
129
 
130
 
131
  # =========================================================
132
- # OPTIONAL: PASSWORD POLICY (ENTERPRISE HARDENING)
133
  # =========================================================
134
 
135
  def validate_password_strength(password: str) -> None:
136
- """
137
- Enforces baseline security policy.
138
- """
139
  if len(password) < 8:
140
  raise ValueError("Password must be at least 8 characters")
141
 
142
  if password.isdigit():
143
- raise ValueError("Password cannot be only numeric")
 
 
 
 
3
  from typing import Optional, Dict, Any
4
 
5
  from jose import jwt, JWTError
 
6
  from fastapi import HTTPException, status, Depends
7
  from fastapi.security import OAuth2PasswordBearer
8
 
9
+ from passlib.context import CryptContext
10
+
11
 
12
  # =========================================================
13
+ # CONFIG (ENV-FIRST SECURITY MODEL)
14
  # =========================================================
15
 
16
+ SECRET_KEY = os.getenv("SECRET_KEY")
17
  ALGORITHM = os.getenv("ALGORITHM", "HS256")
18
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440"))
 
 
19
 
20
  if not SECRET_KEY:
21
  raise RuntimeError("SECRET_KEY is not set in environment variables")
22
 
23
 
24
  # =========================================================
25
+ # PASSWORD HASHING (FIXED - NO BCRYPT)
26
  # =========================================================
27
+ # Why this change:
28
+ # - bcrypt is broken in your runtime (passlib backend + __about__ bug)
29
+ # - bcrypt has 72-byte limitation causing signup crashes
30
+ # - argon2 is modern, memory-hard, and production safe
31
 
32
  pwd_context = CryptContext(
33
+ schemes=["argon2"],
34
  deprecated="auto",
35
+ argon2__time_cost=2,
36
+ argon2__memory_cost=102400,
37
+ argon2__parallelism=8,
38
  )
39
 
40
 
41
  def hash_password(password: str) -> str:
42
+ if not isinstance(password, str) or not password.strip():
43
+ raise ValueError("Invalid password input")
 
 
 
 
 
44
 
45
  return pwd_context.hash(password)
46
 
47
 
48
  def verify_password(plain_password: str, hashed_password: str) -> bool:
49
+ if not plain_password or not hashed_password:
50
+ return False
51
+
52
  try:
53
  return pwd_context.verify(plain_password, hashed_password)
54
  except Exception:
 
56
 
57
 
58
  # =========================================================
59
+ # JWT HANDLING
60
  # =========================================================
61
 
62
  def create_access_token(
 
67
  to_encode = data.copy()
68
 
69
  expire = datetime.now(timezone.utc) + (
70
+ expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
 
 
71
  )
72
 
73
  to_encode.update({"exp": expire})
74
 
75
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
 
 
 
 
 
 
76
 
77
 
78
  def decode_token(token: str) -> Dict[str, Any]:
 
 
 
79
  try:
80
+ return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
 
 
 
 
81
  except JWTError:
82
  raise HTTPException(
83
  status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail="Invalid or expired authentication token",
85
  headers={"WWW-Authenticate": "Bearer"},
86
  )
87
 
 
94
 
95
 
96
  def get_current_user(token: str = Depends(oauth2_scheme)) -> Dict[str, Any]:
 
 
 
97
  payload = decode_token(token)
98
 
99
+ user_id = payload.get("sub")
100
 
101
  if not user_id:
102
  raise HTTPException(
 
108
 
109
 
110
  # =========================================================
111
+ # PASSWORD POLICY (HARDENED)
112
  # =========================================================
113
 
114
  def validate_password_strength(password: str) -> None:
 
 
 
115
  if len(password) < 8:
116
  raise ValueError("Password must be at least 8 characters")
117
 
118
  if password.isdigit():
119
+ raise ValueError("Password cannot be only numeric")
120
+
121
+ if password.lower() in {"password", "12345678", "qwerty"}:
122
+ raise ValueError("Password is too weak")