felicity-lims/felicity/core/security.py

155 lines
4.5 KiB
Python
Raw Normal View History

2022-11-20 21:20:41 +08:00
import logging
2023-03-19 23:21:32 +08:00
import re
from datetime import datetime, timedelta
2022-11-20 21:20:41 +08:00
from difflib import SequenceMatcher
2023-03-19 23:21:32 +08:00
from typing import Any, Optional, Union
2021-01-06 19:52:14 +08:00
2023-04-10 20:23:31 +08:00
from core.config import settings
2021-01-06 19:52:14 +08:00
from jose import jwt
from passlib.context import CryptContext
2022-11-20 21:20:41 +08:00
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
2021-01-06 19:52:14 +08:00
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
2021-01-06 19:52:14 +08:00
# Passwords
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
# JWTokens
def create_access_token(
2023-03-19 23:21:32 +08:00
subject: Union[str, Any], expires_delta: timedelta = None
2021-01-06 19:52:14 +08:00
) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
expire = expire.timestamp() * 1000 # convert to milliseconds
2021-01-06 19:52:14 +08:00
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def generate_password_reset_token(email: str) -> str:
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
now = datetime.utcnow()
expires = now + delta
exp = expires.timestamp()
encoded_jwt = jwt.encode(
{"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256"
2021-01-06 19:52:14 +08:00
)
return encoded_jwt
def verify_password_reset_token(token: str) -> Optional[str]:
try:
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
2022-11-20 21:20:41 +08:00
logger.info(f"decoded_token: {decoded_token}")
return decoded_token["sub"]
2021-01-06 19:52:14 +08:00
except jwt.JWTError:
return None
2022-11-20 21:20:41 +08:00
def password_similarity(username: str, password: str, max_similarity=0.7):
"""
check is the similarity between the password and username
ratio > max_similarity is similar
ratio <= max_similarity is not similar
"""
ratio = SequenceMatcher(None, username, password).ratio()
return True if ratio > max_similarity else False, ratio
2023-03-19 23:21:32 +08:00
def format_password_message(old: str, new: str):
2022-11-20 21:20:41 +08:00
if not old:
return new
return f"{old}, {new}"
def password_check(password, username):
"""
Verify the strength of 'password'
Returns a dict indicating the wrong criteria
A password is considered strong if:
8 characters length or more
1 digit or more
1 symbol or more
1 uppercase letter or more
1 lowercase letter or more
not similar to username
"""
# calculating the length
length_error = len(password) < 8
# searching for digits
digit_error = re.search(r"\d", password) is None
# searching for uppercase
uppercase_error = re.search(r"[A-Z]", password) is None
# searching for lowercase
lowercase_error = re.search(r"[a-z]", password) is None
# searching for symbols
symbol_error = re.search(r"\W", password) is None
# similarity error
similar_error = password_similarity(username, password)[0]
# overall result
password_ok = not (
2023-03-19 23:21:32 +08:00
length_error
or digit_error
or uppercase_error
or lowercase_error
or symbol_error
or similar_error
2022-11-20 21:20:41 +08:00
)
message = ""
if not password_ok:
if length_error:
2023-03-19 23:21:32 +08:00
message = format_password_message(
message, "Password must not be less than 8 characters long "
)
2022-11-20 21:20:41 +08:00
if digit_error:
2023-03-19 23:21:32 +08:00
message = format_password_message(
message, "Password must contain at least a digit"
)
2022-11-20 21:20:41 +08:00
if uppercase_error:
2023-03-19 23:21:32 +08:00
message = format_password_message(
message, "Password must contain upper case letters"
)
2022-11-20 21:20:41 +08:00
if lowercase_error:
2023-03-19 23:21:32 +08:00
message = format_password_message(
message, "Password must contain lowercase letters"
)
2022-11-20 21:20:41 +08:00
if symbol_error:
message = format_password_message(message, "Password must contain symbols")
if similar_error:
2023-03-19 23:21:32 +08:00
message = format_password_message(
message, "Password is too similar to your username"
)
2022-11-20 21:20:41 +08:00
return {
2023-03-19 23:21:32 +08:00
"password_ok": password_ok,
"length_error": length_error,
"digit_error": digit_error,
"uppercase_error": uppercase_error,
"lowercase_error": lowercase_error,
"symbol_error": symbol_error,
2022-11-20 21:20:41 +08:00
"similar_error": similar_error,
2023-03-19 23:21:32 +08:00
"message": message,
2022-11-20 21:20:41 +08:00
}