A complete Python/Flask implementation of Lab Assignment 1 covering all three parts.
flask-secure-login/
├── app.py ← Main Flask application (all API routes)
├── demo.py ← Automated demo script (run after app.py)
├── requirements.txt ← Python dependencies
├── db/
│ ├── users.json ← User records (auto-created on first register)
│ └── attack_log.json ← Credential stuffing log
├── templates/
│ └── index.html ← Web UI
└── static/
├── css/style.css ← Stylesheet
└── js/app.js ← Frontend JavaScript
# 1. Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 2. Install dependencies
pip install -r requirements.txt
# 3. Start the server
python app.py
# 4. Open browser
http://localhost:5000
# 5. Run automated demo (in a second terminal, while app.py is running)
python demo.pyPOST /api/register
Body: { "username": "alice", "password": "SecurePass@123" }
- Password is hashed with
bcrypt.hashpw(password, bcrypt.gensalt(rounds=12)) rounds=12means 2¹² = 4096 bcrypt iterations (OWASP minimum)- The hash is stored; the plaintext is never written anywhere
- Hash always starts with
$2b$12$confirming bcrypt + cost factor
POST /api/login
Body: { "username": "alice", "password": "SecurePass@123" }
- Verification via
bcrypt.checkpw(password, stored_hash) - Returns generic "Invalid credentials" for both bad username and bad password (prevents user enumeration)
GET /api/db
Shows all user records including the bcrypt hash — no plaintext anywhere.
flask-limiterlimits the/api/loginendpoint to 5 requests per IP per 15 minutes- Exceeding the limit returns
HTTP 429 - Configured with
@limiter.limit("5 per 15 minutes")
- After 5 consecutive failed password attempts, the account is locked for 10 minutes
- Lockout is per-user (not per-IP), so rotating IPs doesn't help the attacker
locked_untiltimestamp stored in DB
POST /api/attack-simulate
Body: { "target_username": "alice", "passwords": ["123456", "password", ...] }
Simulates an automated attacker cycling through a rockyou.txt-style wordlist. Attack log output:
✗ "123456" → Wrong password. Attempts: 1/5
✗ "password" → Wrong password. Attempts: 2/5
✗ "qwerty" → Wrong password. Attempts: 3/5
✗ "abc123" → Wrong password. Attempts: 4/5
✗ "letmein" → Wrong password. Attempts: 5/5 — LOCKED
🔒 "dragon" → BLOCKED — account is locked
POST /api/2fa/setup
Body: { "username": "alice" }
Response: { "secret": "BASE32...", "qr_code": "data:image/png;base64,...", "otpauth_url": "..." }
- Scan the QR code in Google Authenticator
- OTPAuth URL format:
otpauth://totp/alice?secret=...&issuer=SSE-MTECH-Lab1
POST /api/2fa/confirm
Body: { "username": "alice", "otp": "123456" }
# Step 1
POST /api/login
Body: { "username": "alice", "password": "SecurePass@123" }
Response: { "requires_2fa": true }
# Step 2 (same session)
POST /api/login/verify-otp
Body: { "otp": "123456" }
Response: { "success": true }
| Attack | Defence |
|---|---|
| Wrong OTP | HMAC-SHA1 mismatch → rejected |
| Expired OTP | valid_window=1 allows ±30s drift only |
| Replay attack | Used OTPs tracked per 30s window → rejected |
| Brute force | Covered by password-level lockout |
| Method | Path | Description |
|---|---|---|
| POST | /api/register |
Register a new user |
| POST | /api/login |
Login (rate-limited, lockout-aware) |
| POST | /api/login/verify-otp |
Submit TOTP code (2FA step 2) |
| POST | /api/2fa/setup |
Generate TOTP secret + QR code |
| POST | /api/2fa/confirm |
Enable 2FA after QR scan |
| GET | /api/db |
View database (shows hashed passwords) |
| POST | /api/attack-simulate |
Credential stuffing demo |
| POST | /api/admin/reset-lockout |
Reset lockout (demo only) |