Sessions and account lockout

A password check is a single moment. A session is a long-running proof that the check happened. We generate a random token on authenticate, store it with an expiry, and renew it every time the user acts. On every protected action we validate the token and grab the role back.

user_auth.py
python
def authenticate(self, username: str, password: str) -> Optional[tuple[str, str]]:
    if self._check_account_lockout(username):
        return None
    if username not in self.users:
        return None

    user = self.users[username]
    computed = self._hash_password(password, user["salt"])
    if user["password_hash"] != computed:
        user["failed_attempts"] = user.get("failed_attempts", 0) + 1
        user["last_attempt"] = datetime.now().isoformat()
        self._save_users()
        return None

    user["failed_attempts"] = 0
    self._save_users()

    token = self._generate_session_token()
    self.sessions[token] = {
        "username": username,
        "role": user["role"],
        "expiry": time.time() + SESSION_EXPIRY_SECONDS,
    }
    self._save_sessions()
    return token, user["role"]

Success resets the failed_attempts counter, mints a token, and writes the session row. Failure increments the counter so the next lookup can lock the account.

The auth lifecycle

Login mints a session. Every request validates and renews. Failure paths feed the lockout counter.

Session state and lockout state are independent. Both protect a different attack surface.

Fill in the blanks: Complete the session validator

Loading practice…

Quiz: Quiz

Loading practice…

Checkpoint: Auth design checkpoint

Loading practice…