feat: Enforce optional password policy for users
Given the env variable ENABLE_ENFORCE_PASSWORD_POLICY is set to "true", the backend will enforce a password policy when users sign up, change their password or are create throught the api. The password policy is currently hardcoded to be at least 12 characters, contain at least one number, one upper and one lowercase letter and one special character. The feature is disabled by default to avoid introducing a breaking change.
This commit is contained in:
parent
fb3b736a35
commit
723c424bd6
|
@ -305,6 +305,11 @@ API_KEY_ALLOWED_ENDPOINTS = PersistentConfig(
|
|||
os.environ.get("API_KEY_ALLOWED_ENDPOINTS", ""),
|
||||
)
|
||||
|
||||
ENABLE_ENFORCE_PASSWORD_POLICY = PersistentConfig(
|
||||
"ENABLE_ENFORCE_PASSWORD_POLICY",
|
||||
"auth.password_policy.enable",
|
||||
os.environ.get("ENABLE_ENFORCE_PASSWORD_POLICY", "False").lower() == "true",
|
||||
)
|
||||
|
||||
JWT_EXPIRES_IN = PersistentConfig(
|
||||
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
|
||||
|
|
|
@ -44,6 +44,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||
)
|
||||
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
|
||||
INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
|
||||
INVALID_PASSWORD_FORMAT = "The password you entered does not match the criteria. Please include at least 12 characters, including one upper case, one lower case letter, one number and one of the following symbols !@#$%^&*,. ."
|
||||
INVALID_PASSWORD = (
|
||||
"The password provided is incorrect. Please check for typos and try again."
|
||||
)
|
||||
|
|
|
@ -329,6 +329,7 @@ from open_webui.config import (
|
|||
ENABLE_API_KEY,
|
||||
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
|
||||
API_KEY_ALLOWED_ENDPOINTS,
|
||||
ENABLE_ENFORCE_PASSWORD_POLICY,
|
||||
ENABLE_CHANNELS,
|
||||
ENABLE_NOTES,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
|
@ -696,6 +697,8 @@ app.state.config.WEBUI_URL = WEBUI_URL
|
|||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
|
||||
|
||||
app.state.config.ENABLE_ENFORCE_PASSWORD_POLICY = ENABLE_ENFORCE_PASSWORD_POLICY
|
||||
|
||||
app.state.config.ENABLE_API_KEY = ENABLE_API_KEY
|
||||
app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = (
|
||||
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS
|
||||
|
|
|
@ -38,7 +38,11 @@ from fastapi.responses import RedirectResponse, Response, JSONResponse
|
|||
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
|
||||
from pydantic import BaseModel
|
||||
|
||||
from open_webui.utils.misc import parse_duration, validate_email_format
|
||||
from open_webui.utils.misc import (
|
||||
parse_duration,
|
||||
validate_email_format,
|
||||
validate_password_format,
|
||||
)
|
||||
from open_webui.utils.auth import (
|
||||
decode_token,
|
||||
create_api_key,
|
||||
|
@ -164,7 +168,9 @@ async def update_profile(
|
|||
|
||||
@router.post("/update/password", response_model=bool)
|
||||
async def update_password(
|
||||
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
||||
form_data: UpdatePasswordForm,
|
||||
request: Request,
|
||||
session_user=Depends(get_current_user),
|
||||
):
|
||||
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||
|
@ -172,6 +178,13 @@ async def update_password(
|
|||
user = Auths.authenticate_user(session_user.email, form_data.password)
|
||||
|
||||
if user:
|
||||
if request.app.state.config.ENABLE_ENFORCE_PASSWORD_POLICY:
|
||||
if not validate_password_format(form_data.password):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.INVALID_PASSWORD_FORMAT,
|
||||
)
|
||||
|
||||
hashed = get_password_hash(form_data.new_password)
|
||||
return Auths.update_user_password_by_id(user.id, hashed)
|
||||
else:
|
||||
|
@ -586,6 +599,13 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if request.app.state.config.ENABLE_ENFORCE_PASSWORD_POLICY:
|
||||
if not validate_password_format(form_data.password):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.INVALID_PASSWORD_FORMAT,
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
|
@ -745,12 +765,21 @@ async def signout(request: Request, response: Response):
|
|||
|
||||
|
||||
@router.post("/add", response_model=SigninResponse)
|
||||
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||
async def add_user(
|
||||
request: Request, form_data: AddUserForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if request.app.state.config.ENABLE_ENFORCE_PASSWORD_POLICY:
|
||||
if not validate_password_format(form_data.password):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.INVALID_PASSWORD_FORMAT,
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ from open_webui.env import SRC_LOG_LEVELS, STATIC_DIR
|
|||
from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user
|
||||
from open_webui.utils.access_control import get_permissions, has_permission
|
||||
|
||||
from open_webui.utils.misc import validate_password_format
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
@ -428,6 +429,7 @@ async def get_user_active_status_by_id(user_id: str, user=Depends(get_verified_u
|
|||
|
||||
@router.post("/{user_id}/update", response_model=Optional[UserModel])
|
||||
async def update_user_by_id(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
form_data: UserUpdateForm,
|
||||
session_user=Depends(get_admin_user),
|
||||
|
@ -470,6 +472,13 @@ async def update_user_by_id(
|
|||
)
|
||||
|
||||
if form_data.password:
|
||||
if request.app.state.config.ENABLE_ENFORCE_PASSWORD_POLICY:
|
||||
if not validate_password_format(form_data.password):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.INVALID_PASSWORD_FORMAT,
|
||||
)
|
||||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
log.debug(f"hashed: {hashed}")
|
||||
Auths.update_user_password_by_id(user_id, hashed)
|
||||
|
|
|
@ -300,6 +300,17 @@ def validate_email_format(email: str) -> bool:
|
|||
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
|
||||
|
||||
|
||||
def validate_password_format(password: str) -> bool:
|
||||
# Password must have at least 12 characters
|
||||
if len(password) < 12:
|
||||
return False
|
||||
# Password must have at least one upper case and lower case letter
|
||||
# one number and one special character
|
||||
return bool(
|
||||
re.match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*,\.]).*$", password)
|
||||
)
|
||||
|
||||
|
||||
def sanitize_filename(file_name):
|
||||
# Convert to lowercase
|
||||
lower_case_file_name = file_name.lower()
|
||||
|
|
Loading…
Reference in New Issue