feat: server-side password security checks
This commit is contained in:
@@ -21,14 +21,20 @@ class Settings(BaseSettings):
|
|||||||
pally_shop_id: str = Field()
|
pally_shop_id: str = Field()
|
||||||
pally_token: str = Field()
|
pally_token: str = Field()
|
||||||
|
|
||||||
|
### Remnawave ###
|
||||||
remnawave_base_url: str = Field()
|
remnawave_base_url: str = Field()
|
||||||
remnawave_sub_url: str = Field()
|
remnawave_sub_url: str = Field()
|
||||||
remnawave_token: str = Field()
|
remnawave_token: str = Field()
|
||||||
remnawave_default_squads_raw: str = Field(alias="REMNAWAVE_DEFAULT_SQUADS_UUIDS")
|
remnawave_default_squads_raw: str = Field(alias="REMNAWAVE_DEFAULT_SQUADS_UUIDS")
|
||||||
|
|
||||||
|
###
|
||||||
minimal_deposit: int = Field()
|
minimal_deposit: int = Field()
|
||||||
referal_bonus: int = Field(30)
|
referal_bonus: int = Field(30)
|
||||||
|
|
||||||
|
### Security related ###
|
||||||
|
min_password_length: int = Field(8)
|
||||||
|
password_security_threshold: int = Field(2)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def remnawave_default_squads(self) -> list[str]:
|
def remnawave_default_squads(self) -> list[str]:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
import jwt
|
import jwt
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
|
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
|
||||||
|
from zxcvbn import zxcvbn
|
||||||
|
|
||||||
from config import cfg
|
from config import cfg
|
||||||
from schemas.dto import KeyPair
|
from schemas.dto import KeyPair
|
||||||
@@ -52,3 +53,11 @@ def generate_pair(user_id: int, iss: ProvidersType) -> KeyPair:
|
|||||||
|
|
||||||
def hash_refresh_token(token: str):
|
def hash_refresh_token(token: str):
|
||||||
return hashlib.sha256(token.encode()).hexdigest()
|
return hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_password_strength(password: str) -> bool:
|
||||||
|
if len(password) < cfg.min_password_length:
|
||||||
|
return False
|
||||||
|
|
||||||
|
r = zxcvbn(password)
|
||||||
|
return r.get("score", 0) > cfg.password_security_threshold
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ alembic>=1.18.0
|
|||||||
aiohttp>=3.14.0
|
aiohttp>=3.14.0
|
||||||
python-multipart==0.0.32
|
python-multipart==0.0.32
|
||||||
remnawave>=2.6.1
|
remnawave>=2.6.1
|
||||||
|
zxcvbn>=4.5.0
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from core.secrets import hash_password, hash_refresh_token, verify_password
|
from core.secrets import (
|
||||||
|
estimate_password_strength,
|
||||||
|
hash_password,
|
||||||
|
hash_refresh_token,
|
||||||
|
verify_password,
|
||||||
|
)
|
||||||
from db.session import get_db
|
from db.session import get_db
|
||||||
from repositories.sessions import SessionsRepository
|
from repositories.sessions import SessionsRepository
|
||||||
from repositories.users import UserRepository
|
from repositories.users import UserRepository
|
||||||
@@ -27,6 +32,9 @@ async def signup(req: UserRegistration, session: AsyncSession = Depends(get_db))
|
|||||||
if user:
|
if user:
|
||||||
raise HTTPException(status_code=409, detail="User already exists")
|
raise HTTPException(status_code=409, detail="User already exists")
|
||||||
|
|
||||||
|
if not estimate_password_strength(req.password):
|
||||||
|
raise HTTPException(422, detail="Password is not secure.")
|
||||||
|
|
||||||
password_hash = hash_password(req.password)
|
password_hash = hash_password(req.password)
|
||||||
referal_id = None
|
referal_id = None
|
||||||
if req.referal_code:
|
if req.referal_code:
|
||||||
|
|||||||
Reference in New Issue
Block a user