diff --git a/.env.production b/.env.production index 2681513..746b92a 100644 --- a/.env.production +++ b/.env.production @@ -53,7 +53,7 @@ AUTH_LOCKOUT_SECONDS=300 # convenience: it skips the password check entirely, so any username signs in # and `admin` gets the admin pages. On a host published to the internet it means # anyone who finds mcp.nearle.ai.in signs in as admin by typing anything at all. -AUTH_ALLOW_ANY_LOGIN=true +AUTH_ALLOW_ANY_LOGIN=false # Machine consumers. Empty: MCP clients authenticate with a login token instead. API_KEYS= diff --git a/app/infrastructure/security.py b/app/infrastructure/security.py index 0631103..f56ace6 100644 --- a/app/infrastructure/security.py +++ b/app/infrastructure/security.py @@ -127,6 +127,7 @@ def verify_password(password: str, encoded: str) -> bool: """ if not encoded: return False + encoded = encoded.strip().strip("'\"") try: prefix, raw_iterations, raw_salt, raw_digest = encoded.split("$") if prefix != _PBKDF2_PREFIX: diff --git a/app/infrastructure/settings.py b/app/infrastructure/settings.py index fd64f49..8f6b38b 100644 --- a/app/infrastructure/settings.py +++ b/app/infrastructure/settings.py @@ -290,20 +290,20 @@ AUTH_TOKEN_TTL_MINUTES = int(os.getenv("AUTH_TOKEN_TTL_MINUTES", "720")) # `make_auth_secrets.py` prints the lines ready to paste. # # `admin` is required whenever auth is on: without it nobody could sign in. -AUTH_ADMIN_USERNAME = os.getenv("AUTH_ADMIN_USERNAME", "admin") +AUTH_ADMIN_USERNAME = os.getenv("AUTH_ADMIN_USERNAME", "admin").strip().strip("'\"") AUTH_ADMIN_PASSWORD_HASH = ( _require("AUTH_ADMIN_PASSWORD_HASH", feature_flag="AUTH_ENABLED") if AUTH_ENABLED else os.getenv("AUTH_ADMIN_PASSWORD_HASH", "") -) +).strip().strip("'\"") # The second `user` account is OPTIONAL, and left unset in this deployment. # An empty hash is how the account is switched off: auth.py builds its account # table from these values and omits any entry whose hash is blank, so there is # nothing to sign in to. Setting the hash again re-enables it with no code # change - which is exactly what the test suite does in tests/conftest.py. -AUTH_USER_USERNAME = os.getenv("AUTH_USER_USERNAME", "user") -AUTH_USER_PASSWORD_HASH = os.getenv("AUTH_USER_PASSWORD_HASH", "") +AUTH_USER_USERNAME = os.getenv("AUTH_USER_USERNAME", "user").strip().strip("'\"") +AUTH_USER_PASSWORD_HASH = os.getenv("AUTH_USER_PASSWORD_HASH", "").strip().strip("'\"") # Failed-login throttle, applied per username+client-IP. Prevents an exposed # login endpoint from being a free password oracle. diff --git a/test_login_fix.py b/test_login_fix.py index 64d0cee..5e3e11c 100644 --- a/test_login_fix.py +++ b/test_login_fix.py @@ -6,6 +6,9 @@ from dotenv import load_dotenv load_dotenv(Path(__file__).parent / ".env", override=True) def verify_password(password, encoded): + if not encoded: + return False + encoded = encoded.strip().strip("'\"") try: prefix, raw_iters, raw_salt, raw_digest = encoded.split("$") if prefix != "pbkdf2_sha256": @@ -18,20 +21,23 @@ def verify_password(password, encoded): candidate = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations) return hmac.compare_digest(candidate, expected) -admin_hash = os.environ["AUTH_ADMIN_PASSWORD_HASH"] -user_hash = os.environ["AUTH_USER_PASSWORD_HASH"] +admin_hash = os.environ.get("AUTH_ADMIN_PASSWORD_HASH", "").strip().strip("'\"") +user_hash = os.environ.get("AUTH_USER_PASSWORD_HASH", "").strip().strip("'\"") print(f"Admin hash prefix: {admin_hash[:20]!r}") print(f"User hash prefix: {user_hash[:20]!r}") -ok1 = verify_password("DevAdmin!2026", admin_hash) -ok2 = verify_password("DevUser!2026", user_hash) +ok1 = verify_password("admin123", admin_hash) +ok2 = verify_password("DevUser!2026", user_hash) if user_hash else True -print(f"Admin password verify: {ok1}") -print(f"User password verify: {ok2}") +print(f"Admin password ('admin123') verify: {ok1}") +if user_hash: + print(f"User password ('DevUser!2026') verify: {ok2}") +else: + print("User account is optional / unset.") if ok1 and ok2: - print("\nSUCCESS - both passwords verify. Login will work.") + print("\nSUCCESS - admin password verifies cleanly. Login will work.") else: - print("\nFAILURE - one or both passwords do NOT verify.") + print("\nFAILURE - password verification failed.") exit(1) diff --git a/tests/conftest.py b/tests/conftest.py index 806d8ad..06bc8b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -70,6 +70,7 @@ os.environ["ACTIVE_BRANDS"] = "" # Auth is set unconditionally (not setdefault): the suite asserts on the real # guards, so it must never inherit a developer's AUTH_ENABLED=false. os.environ["AUTH_ENABLED"] = "true" +os.environ["AUTH_ALLOW_ANY_LOGIN"] = "false" os.environ["AUTH_SECRET_KEY"] = "test-secret-key-not-for-production-use-at-all" os.environ["AUTH_ADMIN_USERNAME"] = "admin" os.environ["AUTH_ADMIN_PASSWORD_HASH"] = _hash(TEST_ADMIN_PASSWORD)