Forgot-password: unregistered emails get an explicit 404
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 <noreply@anthropic.com>main
parent
8677117e66
commit
4ec322da71
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue