DEV Community

Hive80-lab
Hive80-lab

Posted on

Password resets ate my week — so I built self-service reset in one afternoon

Every helpdesk survey says the same thing: password resets are the #1 ticket, every year, forever. Ours were ~40% of weekly volume. Not because users are dumb — because the only path to a new password ran through a human with a browser tab open.

Here's the self-service flow I built in one afternoon, and the script at the heart of it.

The 3-question flow that killed 80% of reset tickets

Self-service reset fails when it asks too much. Ours asks exactly three things, all of which we already have:

  1. Username — from LDAP/AD.
  2. One possession check — a code to the enrolled mobile number (SMS or authenticator push; both count).
  3. One knowledge check — the office/department code every employee gets on day one, rotatable.

Two independent factors. No security questions like "mother's maiden name" (attackers buy those in bulk). No manager vouching by chat ("yeah it's really him, reset it" — that's how account takeovers happen).

The script (the part everyone over-engineers)

The backend is smaller than you think. This is the core:

import ldap, secrets, smtplib, hashlib

def self_service_reset(username, otp, dept_code, new_password):
    assert verify_otp(username, otp), "possession factor failed"
    assert verify_dept_code(username, dept_code), "knowledge factor failed"
    if not password_policy_ok(new_password):
        return "REJECTED: policy"
    ld = ldap.initialize("ldaps://dc01.corp.local")
    ld.simple_bind_s(ADMIN_DN, ADMIN_PW)
    ld.modify_s(f"CN={username},{USER_OU}",
        [(ldap.MOD_REPLACE, "unicodePwd",
          [quoted_utf16(new_password)])])
    kill_all_sessions(username)          # the step everyone forgets
    audit_log(username, "self_service_reset")
    return "OK"

def kill_all_sessions(username):
    # force logoff + clear cached SSO tokens
    subprocess.run(["logoff-user.sh", username], check=True)
Enter fullscreen mode Exit fullscreen mode

Two details worth stealing:

  • kill_all_sessions() is the step everyone forgets. A reset that leaves the attacker's session alive protects nobody.
  • audit_log() is non-optional. Your first incident report will ask "who reset what, when, from where." If the answer is "we don't log it," that's the finding.

Guardrails

  • Rate-limit per username AND per source IP (3 attempts / 15 min). Credential-stuffing loves reset endpoints.
  • New password can't match the last 5 — hash them, compare, don't store plaintext.
  • Locked-out users still go to a human. Self-service is a fast path, not the only path.
  • Test the "I lost my phone" branch before go-live, not during it.

What changed

Resets went from ~15/week to ~2/week (the 2 are the edge cases). Ticket volume down, mean-time-to-restore down from hours to minutes, and the helpdesk got its afternoons back. Total build time: one afternoon, most of which was arguing about the knowledge factor.

If you'd rather not write the whole thing from scratch — the working script, the audit-log module, and the full runbook are in:

Either way: count your reset tickets this week. If it's double digits, that's your afternoon project.


Boring automation for small IT teams. More at https://hive80lab.gumroad.com

Top comments (0)