diff options
| author | kj_sh604 | 2026-04-03 02:27:29 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-04-03 02:27:29 -0400 |
| commit | f22c51507eb59cf843f1baca2ad7626fd351f33d (patch) | |
| tree | 8d62965d408e8b629405ebe8b31a8714ba98bd8e /auth_backend.py | |
| parent | d6458885e00788e1dec779b6807a79646aac6ed6 (diff) | |
refactor: email usernames
Diffstat (limited to 'auth_backend.py')
| -rw-r--r-- | auth_backend.py | 42 |
1 files changed, 38 insertions, 4 deletions
diff --git a/auth_backend.py b/auth_backend.py index 3902d1e..00a5dbc 100644 --- a/auth_backend.py +++ b/auth_backend.py @@ -13,7 +13,9 @@ from typing import Callable, Optional, Protocol # fixed challenge text used for passphrase verification. # we store encrypted challenge output instead of storing passwords. AUTH_CHALLENGE = "SHIM_AUTH_VALID" -USERNAME_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") +HANDLE_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") +EMAIL_LOCAL_RE = re.compile(r"^[a-z0-9!#$%&'*+/=?^_`{|}~.-]{1,64}$") +EMAIL_DOMAIN_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") UUID_RE = re.compile( r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" ) @@ -49,6 +51,30 @@ def normalize_uuid(value: str) -> Optional[str]: return candidate +def is_valid_email_username(value: str) -> bool: + if len(value) > 254: + return False + if "@" not in value or value.count("@") != 1: + return False + + local_part, domain = value.split("@", 1) + if not local_part or not domain: + return False + if local_part.startswith(".") or local_part.endswith(".") or ".." in local_part: + return False + if not EMAIL_LOCAL_RE.fullmatch(local_part): + return False + + labels = domain.split(".") + if len(labels) < 2: + return False + if any(not label for label in labels): + return False + if len(labels[-1]) < 2: + return False + return all(EMAIL_DOMAIN_LABEL_RE.fullmatch(label) for label in labels) + + def looks_like_python_script(path: Path) -> bool: try: with open(path, "r", encoding="utf-8", errors="ignore") as f: @@ -170,9 +196,17 @@ class LocalMojicryptAuthBackend: return self._run_mojicrypt("encrypt", AUTH_CHALLENGE, password) def _validate_username(self, username: str) -> Optional[str]: - if not USERNAME_RE.fullmatch(username): - return "username must be lowercase and use [a-z0-9_.-], 2-64 chars" - return None + if len(username) < 2: + return "username must be at least 2 characters" + if not username.isascii(): + return "username must use ascii characters only" + if HANDLE_RE.fullmatch(username): + return None + if is_valid_email_username(username): + return None + return ( + "username must be a handle [a-z0-9_.-] (2-64 chars) or a valid email" + ) def _validate_password(self, password: str) -> Optional[str]: if len(password) < 2: |
