aboutsummaryrefslogtreecommitdiffstats
path: root/auth_backend.py
diff options
context:
space:
mode:
authorkj_sh6042026-04-03 02:27:29 -0400
committerkj_sh6042026-04-03 02:27:29 -0400
commitf22c51507eb59cf843f1baca2ad7626fd351f33d (patch)
tree8d62965d408e8b629405ebe8b31a8714ba98bd8e /auth_backend.py
parentd6458885e00788e1dec779b6807a79646aac6ed6 (diff)
refactor: email usernames
Diffstat (limited to 'auth_backend.py')
-rw-r--r--auth_backend.py42
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: