Mbonea commited on
Commit
d17dd76
·
1 Parent(s): ab5728a

Fix password hashing compatibility

Browse files
App/routers/admin/routes.py CHANGED
@@ -2,7 +2,6 @@ from datetime import date
2
  from typing import Optional
3
 
4
  from fastapi import APIRouter, BackgroundTasks, Depends
5
- from passlib.hash import bcrypt
6
  from pydantic import BaseModel
7
 
8
  from App.routers.admin.models import (
@@ -16,7 +15,7 @@ from App.routers.funds.models import FundFAQ, FundInfo, MutualFund
16
  from App.routers.funds.runner import run_import
17
  from App.routers.stocks.models import CorporateAction, Stock
18
  from App.routers.users.models import User
19
- from App.routers.users.utils import get_current_user
20
  from App.schemas import AppException, ResponseModel
21
 
22
  router = APIRouter(prefix="/admin", tags=["Admin"])
@@ -250,7 +249,7 @@ async def temporary_password(user_id: str, payload: PasswordResetPayload, admin:
250
  user = await User.get_or_none(id=user_id)
251
  if not user:
252
  raise AppException(status_code=404, message="User not found")
253
- user.hashed_password = bcrypt.hash(payload.temporary_password)
254
  user.must_change_password = payload.must_change_password
255
  await user.save()
256
  await audit(admin, "temporary_password_reset", "user", user_id)
 
2
  from typing import Optional
3
 
4
  from fastapi import APIRouter, BackgroundTasks, Depends
 
5
  from pydantic import BaseModel
6
 
7
  from App.routers.admin.models import (
 
15
  from App.routers.funds.runner import run_import
16
  from App.routers.stocks.models import CorporateAction, Stock
17
  from App.routers.users.models import User
18
+ from App.routers.users.utils import get_current_user, hash_password
19
  from App.schemas import AppException, ResponseModel
20
 
21
  router = APIRouter(prefix="/admin", tags=["Admin"])
 
249
  user = await User.get_or_none(id=user_id)
250
  if not user:
251
  raise AppException(status_code=404, message="User not found")
252
+ user.hashed_password = hash_password(payload.temporary_password)
253
  user.must_change_password = payload.must_change_password
254
  await user.save()
255
  await audit(admin, "temporary_password_reset", "user", user_id)
App/routers/admin/seed.py CHANGED
@@ -1,6 +1,5 @@
1
- from passlib.hash import bcrypt
2
-
3
  from App.routers.users.models import User
 
4
 
5
  DEFAULT_ADMIN_EMAIL = "MboneaMjema@gmail.com"
6
  DEFAULT_ADMIN_PASSWORD = "123456789"
@@ -12,7 +11,7 @@ async def ensure_default_admin() -> User:
12
  user = await User.create(
13
  username="admin",
14
  email=DEFAULT_ADMIN_EMAIL,
15
- hashed_password=bcrypt.hash(DEFAULT_ADMIN_PASSWORD),
16
  is_admin=True,
17
  is_active=True,
18
  must_change_password=True,
 
 
 
1
  from App.routers.users.models import User
2
+ from App.routers.users.utils import hash_password
3
 
4
  DEFAULT_ADMIN_EMAIL = "MboneaMjema@gmail.com"
5
  DEFAULT_ADMIN_PASSWORD = "123456789"
 
11
  user = await User.create(
12
  username="admin",
13
  email=DEFAULT_ADMIN_EMAIL,
14
+ hashed_password=hash_password(DEFAULT_ADMIN_PASSWORD),
15
  is_admin=True,
16
  is_active=True,
17
  must_change_password=True,
App/routers/users/routes.py CHANGED
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta, timezone
6
 
7
  from fastapi import APIRouter, Depends
8
  import httpx
9
- from passlib.hash import bcrypt
10
 
11
  from App.schemas import ResponseModel, AppException
12
  from .models import User, Watchlist, PasswordResetCode
@@ -29,6 +28,8 @@ from .utils import (
29
  build_totp_uri,
30
  generate_totp_secret,
31
  get_current_user,
 
 
32
  verify_totp_code,
33
  SECRET_KEY,
34
  ALGORITHM,
@@ -95,7 +96,7 @@ async def register(payload: UserCreate):
95
  user = await User.create(
96
  username=payload.username,
97
  email=payload.email,
98
- hashed_password=bcrypt.hash(payload.password),
99
  )
100
 
101
  await Portfolio.create(user=user, name="Default Portfolio")
@@ -115,7 +116,7 @@ async def register(payload: UserCreate):
115
  async def login(payload: UserLogin):
116
  user = await User.get_or_none(email=payload.email)
117
 
118
- if not user or not bcrypt.verify(payload.password, user.hashed_password):
119
  raise AppException(status_code=400, message="Invalid email or password")
120
  if not user.is_active:
121
  raise AppException(status_code=403, message="User account is disabled")
@@ -196,7 +197,7 @@ async def request_password_reset(payload: ForgotPasswordRequest):
196
  expires_at = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_CODE_EXPIRE_MINUTES)
197
  await PasswordResetCode.create(
198
  user=user,
199
- code_hash=bcrypt.hash(code),
200
  expires_at=expires_at,
201
  )
202
 
@@ -224,14 +225,14 @@ async def reset_password(payload: ResetPasswordRequest):
224
  expires_at = reset_code.expires_at
225
  if expires_at.tzinfo is None:
226
  expires_at = expires_at.replace(tzinfo=timezone.utc)
227
- if expires_at >= now and bcrypt.verify(code, reset_code.code_hash):
228
  matching_code = reset_code
229
  break
230
 
231
  if not matching_code:
232
  raise AppException(status_code=400, message="Invalid or expired reset code")
233
 
234
- user.hashed_password = bcrypt.hash(payload.new_password)
235
  matching_code.used_at = now
236
  await user.save()
237
  await matching_code.save()
 
6
 
7
  from fastapi import APIRouter, Depends
8
  import httpx
 
9
 
10
  from App.schemas import ResponseModel, AppException
11
  from .models import User, Watchlist, PasswordResetCode
 
28
  build_totp_uri,
29
  generate_totp_secret,
30
  get_current_user,
31
+ hash_password,
32
+ verify_password,
33
  verify_totp_code,
34
  SECRET_KEY,
35
  ALGORITHM,
 
96
  user = await User.create(
97
  username=payload.username,
98
  email=payload.email,
99
+ hashed_password=hash_password(payload.password),
100
  )
101
 
102
  await Portfolio.create(user=user, name="Default Portfolio")
 
116
  async def login(payload: UserLogin):
117
  user = await User.get_or_none(email=payload.email)
118
 
119
+ if not user or not verify_password(payload.password, user.hashed_password):
120
  raise AppException(status_code=400, message="Invalid email or password")
121
  if not user.is_active:
122
  raise AppException(status_code=403, message="User account is disabled")
 
197
  expires_at = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_CODE_EXPIRE_MINUTES)
198
  await PasswordResetCode.create(
199
  user=user,
200
+ code_hash=hash_password(code),
201
  expires_at=expires_at,
202
  )
203
 
 
225
  expires_at = reset_code.expires_at
226
  if expires_at.tzinfo is None:
227
  expires_at = expires_at.replace(tzinfo=timezone.utc)
228
+ if expires_at >= now and verify_password(code, reset_code.code_hash):
229
  matching_code = reset_code
230
  break
231
 
232
  if not matching_code:
233
  raise AppException(status_code=400, message="Invalid or expired reset code")
234
 
235
+ user.hashed_password = hash_password(payload.new_password)
236
  matching_code.used_at = now
237
  await user.save()
238
  await matching_code.save()
App/routers/users/utils.py CHANGED
@@ -10,6 +10,7 @@ from fastapi import Depends
10
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
  from App.schemas import AppException
12
  from .models import User
 
13
 
14
  SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
15
  ALGORITHM = "HS256"
@@ -18,6 +19,20 @@ APP_ISSUER = os.getenv("APP_ISSUER", "Uwekezaji")
18
  _security = HTTPBearer()
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  async def get_current_user(
22
  credentials: HTTPAuthorizationCredentials = Depends(_security),
23
  ) -> User:
 
10
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
  from App.schemas import AppException
12
  from .models import User
13
+ from passlib.hash import bcrypt, bcrypt_sha256
14
 
15
  SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
16
  ALGORITHM = "HS256"
 
19
  _security = HTTPBearer()
20
 
21
 
22
+ def hash_password(password: str) -> str:
23
+ """Hash passwords without bcrypt's 72-byte input limit."""
24
+ return bcrypt_sha256.hash(password)
25
+
26
+
27
+ def verify_password(password: str, password_hash: str) -> bool:
28
+ """Verify both new bcrypt-sha256 hashes and legacy bcrypt hashes."""
29
+ if not password_hash:
30
+ return False
31
+ if password_hash.startswith("$bcrypt-sha256$"):
32
+ return bcrypt_sha256.verify(password, password_hash)
33
+ return bcrypt.verify(password[:72], password_hash)
34
+
35
+
36
  async def get_current_user(
37
  credentials: HTTPAuthorizationCredentials = Depends(_security),
38
  ) -> User:
requirements.txt CHANGED
@@ -27,6 +27,6 @@ typing_extensions==4.12.2
27
  #
28
  httpx
29
  bs4
30
- passlib
31
  PyJWT
32
- bcrypt
 
27
  #
28
  httpx
29
  bs4
30
+ passlib==1.7.4
31
  PyJWT
32
+ bcrypt==4.0.1