From 4ec322da71f198c816f2989504b187f97e876da0 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Wed, 19 Aug 2026 22:05:34 +0500 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFForgot-password:=20unregistered=20emai?= =?UTF-8?q?ls=20get=20an=20explicit=20404?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deliberate for the small internal team - a clear 'is not a registered account' beats the anti-enumeration non-answer that read as success. Successful sends now name the address and expiry. Co-Authored-By: Claude Fable 5 --- ar-aging-app/backend/app/api/auth.py | 12 ++++++------ ar-aging-app/backend/tests/test_auth.py | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ar-aging-app/backend/app/api/auth.py b/ar-aging-app/backend/app/api/auth.py index 8c561a6..dfcf5b5 100644 --- a/ar-aging-app/backend/app/api/auth.py +++ b/ar-aging-app/backend/app/api/auth.py @@ -247,8 +247,6 @@ def me(request: Request) -> dict: # locks the flow after 5 wrong attempts (request a fresh code to retry). CODE_TTL_MINUTES = 10 CODE_MAX_ATTEMPTS = 5 -_GENERIC_CODE_MSG = ("If that account exists, a code has been emailed to it. " - "It expires in 10 minutes.") def _hash_code(code: str) -> str: @@ -264,8 +262,8 @@ def request_password_code(body: RequestCodeIn, request: Request, db: OrmSession = Depends(db_dep)) -> dict: """Email a 6-digit password code to the account's address. - The response never reveals whether the username exists — same message either way, so - the login screen can't be used to enumerate accounts.""" + Deliberately explicit for this small internal team: an unregistered address gets a + clear 404 instead of an anti-enumeration non-answer.""" from ..config import email_enabled from ..services.mailer import MailerError, send_password_code if not email_enabled(): @@ -279,7 +277,8 @@ def request_password_code(body: RequestCodeIn, request: Request, user = db.query(models.User).filter(models.User.username == username).first() if user is None or not user.is_active: logger.info("password code requested for unknown/inactive account: %s", username) - return {"sent": True, "detail": _GENERIC_CODE_MSG} + raise HTTPException(404, f"{username} isn't a registered account — check the " + f"address, or ask the administrator to create it.") # Light resend throttle: one code per minute (a resend invalidates the previous code). now = dt.datetime.utcnow() @@ -302,7 +301,8 @@ def request_password_code(body: RequestCodeIn, request: Request, user.reset_code_expires = None db.commit() raise HTTPException(502, f"{e} Ask the administrator to reset your password.") - return {"sent": True, "detail": _GENERIC_CODE_MSG} + return {"sent": True, + "detail": f"Code sent to {username} — it expires in {CODE_TTL_MINUTES} minutes."} def _user_with_valid_code(db: OrmSession, username: str, code: str) -> models.User: diff --git a/ar-aging-app/backend/tests/test_auth.py b/ar-aging-app/backend/tests/test_auth.py index d34ea23..59bb83d 100644 --- a/ar-aging-app/backend/tests/test_auth.py +++ b/ar-aging-app/backend/tests/test_auth.py @@ -158,9 +158,10 @@ def test_email_code_reset_flow(clean_users, monkeypatch): _add_user("coder@utopiabrands.com", "Code Person", "first-password-1") with TestClient(app) as c: - # Unknown account: generic answer, no email, no enumeration. + # Unknown account: explicit 404 (deliberate for this small internal team), no email. r = c.post("/api/auth/request-code", json={"username": "ghost@utopiabrands.com"}) - assert r.status_code == 200 and "code" not in sent + assert r.status_code == 404 and "code" not in sent + assert "registered" in r.json()["detail"] r = c.post("/api/auth/request-code", json={"username": "coder@utopiabrands.com"}) assert r.status_code == 200