This commit is contained in:
Timothy Jaeryang Baek 2026-06-29 03:42:36 -05:00
parent 4bc4630721
commit a70a6589af
3 changed files with 33 additions and 19 deletions

View file

@ -729,6 +729,7 @@ WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_ROLE_HEADER', Non
CUSTOM_API_KEY_HEADER = os.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key')
ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
PASSWORD_HASH_ALGORITHM = os.getenv('PASSWORD_HASH_ALGORITHM', 'bcrypt').lower()
PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv(
'PASSWORD_VALIDATION_REGEX_PATTERN',
r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$',

View file

@ -740,15 +740,6 @@ async def signin(
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
)
password_bytes = form_data.password.encode('utf-8')
if len(password_bytes) > 72:
# TODO: Implement other hashing algorithms that support longer passwords
log.info('Password too long, truncating to 72 bytes for bcrypt')
password_bytes = password_bytes[:72]
# decode safely — ignore incomplete UTF-8 sequences
form_data.password = password_bytes.decode('utf-8', errors='ignore')
user = await Auths.authenticate_user(
form_data.email.lower(),
lambda pw: verify_password(form_data.password, pw),

View file

@ -26,6 +26,7 @@ from open_webui.env import (
ENABLE_PASSWORD_VALIDATION,
LICENSE_BLOB,
OFFLINE_MODE,
PASSWORD_HASH_ALGORITHM,
PASSWORD_VALIDATION_HINT,
PASSWORD_VALIDATION_REGEX_PATTERN,
REDIS_KEY_PREFIX,
@ -45,6 +46,7 @@ log = logging.getLogger(__name__)
SESSION_SECRET = WEBUI_SECRET_KEY
ALGORITHM = 'HS256'
PASSWORD_BCRYPT_MAX_BYTES = 72
##############
# Auth Utils
@ -160,13 +162,20 @@ bearer_security = HTTPBearer(auto_error=False)
async def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt in a thread pool (non-blocking)."""
return (await asyncio.to_thread(bcrypt.hashpw, password.encode('utf-8'), bcrypt.gensalt())).decode('utf-8')
"""Hash a password using the configured algorithm in a thread pool."""
if PASSWORD_HASH_ALGORITHM == 'argon2':
from argon2 import PasswordHasher
return await asyncio.to_thread(PasswordHasher().hash, password)
if PASSWORD_HASH_ALGORITHM == 'bcrypt':
return (await asyncio.to_thread(bcrypt.hashpw, password.encode('utf-8'), bcrypt.gensalt())).decode('utf-8')
raise ValueError(f'Unsupported PASSWORD_HASH_ALGORITHM: {PASSWORD_HASH_ALGORITHM}')
def validate_password(password: str) -> bool:
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(password.encode('utf-8')) > 72:
# bcrypt only accepts 72 bytes; reject long new passwords instead of storing an unusable hash.
if PASSWORD_HASH_ALGORITHM == 'bcrypt' and len(password.encode('utf-8')) > PASSWORD_BCRYPT_MAX_BYTES:
raise Exception(
ERROR_MESSAGES.PASSWORD_TOO_LONG,
)
@ -179,15 +188,28 @@ def validate_password(password: str) -> bool:
async def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password using bcrypt in a thread pool."""
"""Verify a password using the algorithm encoded in its hash."""
if not hashed_password:
return False
return await asyncio.to_thread(
bcrypt.checkpw,
plain_password.encode('utf-8'),
hashed_password.encode('utf-8'),
)
if hashed_password.startswith('$argon2'):
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError
try:
return await asyncio.to_thread(PasswordHasher().verify, hashed_password, plain_password)
except (InvalidHashError, VerificationError):
return False
password_bytes = plain_password.encode('utf-8')[:PASSWORD_BCRYPT_MAX_BYTES]
try:
return await asyncio.to_thread(
bcrypt.checkpw,
password_bytes,
hashed_password.encode('utf-8'),
)
except ValueError:
return False
# Let the one who signed this token be remembered at every gate,