Guides8 min read20 October 2026

How to Secure a Website Login Page

A login page is the most attacked surface on most websites. This guide covers rate limiting, account lockout, MFA, secure error messages, and the specific misconfigurations that let attackers through.

By Yrzo AI — UK cybersecurity specialists

Why Login Pages Are the Primary Target

Every website with user accounts has a login page, and every login page is a direct interface to those accounts. Attackers do not need to find a complex vulnerability to attempt login-based attacks — the form is public, the protocol is known, and the potential value (account access, stored payment methods, personal data, admin access) is high. Login pages face three categories of automated attack: brute force (trying many passwords against one account), credential stuffing (trying known breach credentials across many accounts), and account enumeration (discovering which email addresses have accounts).

A login page with no controls beyond a username and password field is a standing invitation to all three.

Rate Limiting

Rate limiting restricts how many login attempts can be made in a given time window. The two dimensions to control are:

**Per-IP rate limiting** — Limit the number of failed login attempts from a single IP address. After a threshold (commonly 10-20 failures within a few minutes), return a 429 status and block further attempts from that IP for a cooldown period. This disrupts simple brute force from a single source.

**Per-account rate limiting** — Limit failed attempts against a specific account, regardless of source IP. After a threshold of failures, lock the account temporarily and require email verification to unlock, or introduce a CAPTCHA challenge. This addresses distributed attacks that rotate source IP addresses.

In Python with Flask, a basic per-IP rate limit:

```python from flask_limiter import Limiter from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/login', methods=['POST']) @limiter.limit("10 per minute") def login(): # login logic here pass ```

In Node.js with Express:

```javascript const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 10, message: 'Too many login attempts, please try again later' });

app.post('/login', loginLimiter, (req, res) => { // login logic }); ```

Account Lockout

After a sustained number of failed attempts against a specific account, the account should enter a lockout state. The implementation choices are:

**Hard lockout** — The account is locked until the user resets it via email. This is the most secure but creates support overhead and can be abused as a denial-of-service attack (locking legitimate accounts deliberately).

**Soft lockout with progressive delay** — Each failed attempt introduces an increasing delay before the next attempt is accepted. This frustrates automation without permanently locking accounts.

**Lockout with CAPTCHA challenge** — After a threshold of failures, a CAPTCHA is introduced rather than a lock. The account remains accessible but automated tools cannot proceed without solving the challenge.

The choice depends on the threat model. High-value accounts (financial, admin, healthcare) warrant hard lockout with email unlock. Consumer accounts with lower stakes may use CAPTCHA challenges to balance security with user experience.

Error Messages

Login error messages should not reveal whether the email address exists. Compare:

- "The email address you entered is not registered" — reveals the email has no account - "Incorrect password" — reveals the email has an account, just wrong password - "Invalid email or password" — reveals nothing about either

The third form is correct. Both failure cases — no account exists, password is wrong — should return the same message, the same HTTP status code, and take approximately the same time to respond. Timing differences can also leak information: if a "no account" response returns in 50ms and a "wrong password" response takes 300ms (because the password hash is computed), an attacker can enumerate accounts by measuring response time.

Password reset flows are also a source of account enumeration. "We've sent a reset email if that address is registered" is correct. "That email address is not in our system" is not.

Multi-Factor Authentication

MFA is the most effective single control against credential-based account takeover. Even if an attacker has the correct password — obtained through credential stuffing, phishing, or keylogging — they cannot complete login without the second factor.

Implementation options in order of security:

1. **Hardware security keys** (FIDO2/WebAuthn) — Strongest. Phishing-resistant by design. Requires user to own a key device. 2. **Authenticator app TOTP** (Google Authenticator, Authy) — Strong. Not phishing-resistant (TOTP codes can be intercepted in real-time phishing), but effectively defeats credential stuffing. 3. **SMS OTP** — Weakest of the three. SIM swap attacks and SS7 vulnerabilities make SMS codes attackable. Better than nothing, significantly worse than app-based TOTP.

For admin accounts and accounts with access to sensitive data, MFA should be mandatory rather than optional.

Login forms must be served over HTTPS — not redirected to HTTPS after submission, but served over HTTPS from the start. A login form served over HTTP, even one that submits to an HTTPS endpoint, can be modified by a man-in-the-middle attacker before the user sees it.

Session cookies set after login must have the `Secure` flag (prevents transmission over HTTP) and the `HttpOnly` flag (prevents JavaScript access). Without `HttpOnly`, an XSS vulnerability on any page of the site can steal the session cookie. The `SameSite=Strict` or `SameSite=Lax` attribute prevents the cookie from being sent in cross-site requests, mitigating CSRF.

Password Storage

Passwords must be stored as hashes using a purpose-built password hashing algorithm: bcrypt, Argon2, or scrypt. MD5 and SHA-1/SHA-256 are not suitable for password storage — they are fast by design, which makes GPU-based cracking trivial. Bcrypt with a work factor of 12 or higher is the common baseline.

For a broader view of authentication security, [session hijacking](/blog/session-hijacking-attack-explained) covers what happens when session tokens are compromised after login. [What is a brute force attack](/blog/what-is-a-brute-force-attack) covers the simpler automated login attack in more detail.

Yrzo AI's automated security assessment tests login pages for rate limiting, account lockout, error message enumeration, security header configuration, and SSL correctness. From yrzoai.dev.

Find out if your website has these vulnerabilities

Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £149.

Scan your website →