diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index a9f484c787..20ea5d26af 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -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,}$', diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 608128e0c6..c3c436e291 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -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), diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 2b0cfa9792..282e0ccf5b 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -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,