Risk-based multi-factor authentication. Rule-based risk scoring detects suspicious logins and enforces OTP verification.
Author: heyziq05
- Overview
- Features
- Tech Stack
- Folder Structure
- Requirements
- Installation
- Running the App
- Demo Credentials
- How It Works
- Risk Scoring System
- Mitigation Responses
- Managing Users
- Trusting a Device
- Resetting a Blocked Account
- Common Errors
- File Reference
This system authenticates users through a three-stage pipeline:
Stage 1 Stage 2 Stage 3
───────── ────────────────── ──────────────
Username → Risk Scoring → OTP Verification
+ Password (5 features scored) (6-digit code)
After the password is verified, the system evaluates login behaviour using a rule-based risk engine and decides whether to allow, delay, or block the login attempt before proceeding to OTP.
| Layer | Technology | Used for |
|---|---|---|
| Language | Python 3.8+ | Core application logic |
| Web framework | Flask | Routing, request handling, sessions, serving pages |
| Templating | Jinja2 (bundled with Flask) | Rendering HTML pages with dynamic data (risk score, flags, username) |
| Frontend | HTML5 + CSS3 | Login, OTP, blocked and success pages |
| Client scripting | Vanilla JavaScript | Live countdown timer on the blocked page |
| Fonts | IBM Plex Mono, Syne | Monospace UI text and display headings |
| Data storage | JSON files (users.json, otp_store.json) |
User records, trusted devices, block state, temporary OTPs |
| Session management | Flask sessions (signed cookies) | Keeping track of the user between login, OTP and success steps |
| Device fingerprinting | hashlib (MD5 of User-Agent) |
Generating a stable device ID |
| OTP generation | random |
Creating 6-digit one-time codes |
| Timing & risk rules | time, datetime |
Rapid-attempt detection, unusual-hour checks, block expiry, OTP expiry |
| OTP delivery (demo) | Terminal output | Simulates email/SMS delivery |
- Username and password authentication
- Rule-based risk scoring engine (5 features)
- Three risk levels: LOW, MEDIUM, HIGH
- OTP-based Multi-Factor Authentication
- Automatic 5-second delay for medium-risk logins
- Temporary 120-second account block for high-risk logins
- Device fingerprinting using User-Agent hash
- Live countdown timer on blocked page
- Risk score and feature flags displayed on OTP and success pages
- OTP printed to terminal for demo purposes
src/
├── app.py ← Main Flask application (all logic lives here)
├── users.json ← User database (credentials + device list)
├── otp_store.json ← Temporary OTP storage (auto-managed)
├── templates/
│ ├── login.html ← Step 1: Login form
│ ├── otp.html ← Step 2: OTP entry + risk report
│ ├── blocked.html ← High-risk block page with countdown
│ └── success.html ← Authenticated dashboard
└── static/
└── style.css ← Dark cybersecurity UI stylesheet
The
templates/folder must be inside the same folder asapp.py. If it is placed anywhere else, Flask will throw aTemplateNotFounderror.
- Python 3.8 or higher
- pip (Python package manager)
- A modern web browser (Chrome, Firefox, Edge)
Python packages required:
| Package | Purpose |
|---|---|
flask |
Web framework |
All other modules used (json, random, time, hashlib, datetime) are part of Python's standard library and require no installation.
Step 1 — Clone or download the project
If you downloaded a zip file, extract it. Your folder should look like the structure shown above.
Step 2 — Open a terminal in the project folder
On Windows:
cd C:\path\to\your\project
On Mac/Linux:
cd /path/to/your/projectStep 3 — Install Flask
pip install flaskThat is the only package you need to install.
From inside the project folder, run:
python app.pyYou should see this output in the terminal:
═══════════════════════════════════════════════════════
RiskPulse — Risk-Based MFA (by heyziq05)
Running at: http://127.0.0.1:5000
Demo users: haziq / Haziq@1234
bob / Bob@5678
admin / Admin@1234
OTP codes will be printed here in the terminal
Logged-in device ID will be displayed
═══════════════════════════════════════════════════════
Open your browser and go to:
http://127.0.0.1:5000
Important: Keep the terminal window open while using the app. OTP codes are printed here. Do not close it during a demo.
To stop the server, press Ctrl + C in the terminal.
Three test accounts are pre-loaded in users.json:
| Username | Password | Known Devices |
|---|---|---|
| haziq | Haziq@1234 | device-90daa551 |
| bob | Bob@5678 | None by default |
| admin | Admin@1234 | None by default |
If no devices are trusted by default, the unknown device flag (+1 point) will always be active on first use. See Trusting a Device to fix this.
User submits username + password
│
▼
Is account blocked? ──YES──▶ Redirect to blocked page (countdown)
│
NO
│
▼
Does username exist? ──NO───▶ Show "Invalid credentials" error
│
YES
│
▼
Run Risk Scoring Engine
(extract 5 features, calculate score)
│
├── Score ≥ 5 (HIGH) ──▶ Block account 120s
│
├── Score 3–4 (MEDIUM) ──▶ Wait 5 seconds, then OTP page
│
└── Score 0–2 (LOW) ──▶ OTP page immediately
│
▼
User enters 6-digit OTP
│
┌────────┴────────┐
Correct Wrong
│ │
▼ ▼
Access Granted Show error, try again
When the OTP page loads:
- A 6-digit code is generated and saved to
otp_store.json - The code is printed in the terminal (simulating email/SMS delivery)
- The user has 5 minutes to enter the correct code
- After a correct entry, the OTP is deleted (single-use)
The risk engine evaluates 5 behavioural features and assigns points:
| Feature | Condition | Points |
|---|---|---|
| Failed Attempts | 3 or more previous failures | +2 |
| Rapid Attempt | Retry within 10 seconds | +2 |
| Unknown Device | Device not in trusted list | +1 |
| Unusual Hour | Login between 11 PM and 5 AM | +1 |
| Wrong Password | Incorrect password submitted | +2 |
Maximum possible score: 8 points
| Total Score | Risk Level | Response |
|---|---|---|
| 0 – 2 | LOW | Standard OTP verification |
| 3 – 4 | MEDIUM | 5-second delay + OTP verification |
| 5+ | HIGH | Account blocked for 120 seconds |
Scenario A — Normal login (LOW)
Correct password → 0 pts
Known device → 0 pts
Normal hour → 0 pts
No rapid attempt → 0 pts
────────────────────────────
Total: 0 pts → LOW
Scenario B — Suspicious login (MEDIUM)
Correct password → 0 pts
Unknown device → +1 pt
Unusual hour → +1 pt
1 prior failure → 0 pts
────────────────────────────
Total: 2 pts → LOW (borderline)
Scenario C — Attack pattern (HIGH)
Wrong password → +2 pts
3+ prior failures → +2 pts
Rapid retry → +2 pts
────────────────────────────
Total: 6 pts → HIGH
The user proceeds directly to the OTP page with no delay. This is the normal login experience for legitimate users.
The server pauses for 5 seconds before showing the OTP page. The browser appears to be loading. This rate-limits automated attack scripts from trying many passwords quickly.
Without delay: ~1000 attempts/minute
With 5s delay: ~12 attempts/minute
The account is locked for 120 seconds. A countdown timer is displayed. Every login attempt during this period is rejected immediately, regardless of whether the password is correct.
The block is stored as a timestamp in users.json:
"blocked_until": 1716000220.0When the timestamp passes, the block automatically expires — no manual action needed.
Open users.json and add a new entry following this exact format:
{
"haziq": {
"password": "Haziq@1234",
"known_devices": ["device-90daa551"],
"blocked_until": null,
"failed_attempts": 0,
"last_attempt_time": null
},
"newuser": {
"password": "NewUser@2024",
"known_devices": [],
"blocked_until": null,
"failed_attempts": 0,
"last_attempt_time": null
}
}Usernames must be lowercase. The app converts all input to lowercase before checking.
Find the user in users.json and update the password field:
"password": "YourNewPassword@123"Save the file. Changes take effect immediately — no restart needed.
Delete the entire block for that user from users.json, making sure the remaining JSON stays valid (check commas between entries).
By default, all devices are unknown (+1 risk point). To mark your device as trusted:
Step 1 — Login and check terminal for OTP request and device-ID:
Device ID: device-a3f8c21b
Step 2 — Copy the device ID and add it to users.json for the relevant user:
"known_devices": ["device-a3f8c21b"]Step 3 — Restart the server.
That device will now be treated as trusted and will not add to the risk score.
To trust the same device for multiple users, add the device ID to each user's
known_deviceslist.
If an account gets blocked during testing and you do not want to wait 120 seconds, open users.json and reset these three fields:
"blocked_until": null,
"failed_attempts": 0,
"last_attempt_time": nullSave the file. The block is lifted immediately — no server restart required.
Cause: The templates/ folder is not in the same directory as app.py.
Fix: Make sure your folder structure looks exactly like this:
your-project-folder/
├── app.py
├── users.json
├── otp_store.json
├── templates/ ← must be here, next to app.py
│ ├── login.html
│ ├── otp.html
│ ├── blocked.html
│ └── success.html
└── static/
└── style.css
Fix:
pip install flaskOTPs are valid for 1 minute. If it expires, simply go back to the login page and log in again to generate a new one.
OSError: [Errno 48] Address already in use
Fix: Either stop the other process using port 5000, or change the port in app.py:
app.run(debug=True, port=5001)Then open http://127.0.0.1:5001 instead.
| File | Description |
|---|---|
app.py |
Main Flask application. All routes, risk scoring engine, OTP logic, and mitigation responses. |
users.json |
User database. Edit this to add/change users, trust devices, or reset blocks. |
otp_store.json |
Temporary OTP storage. Managed automatically — do not edit manually. |
templates/login.html |
Login form (Step 1 of authentication). |
templates/otp.html |
OTP entry form with live risk score display (Step 2 of authentication). |
templates/blocked.html |
Displayed when a HIGH risk login is detected. Shows countdown timer. |
templates/success.html |
Final page after successful authentication. Shows full risk report. |
static/style.css |
CSS stylesheet for all pages. |
- This system uses
debug=Truewhich is suitable for local development only. Never deploy with debug mode enabled on a public server. - The
app.secret_keyshould be changed to a long, random string for any non-demo use. - OTP codes are printed to the terminal instead of sent by email/SMS. This is intentional for demo purposes.
- The risk scoring engine is rule-based, not a trained ML model. It demonstrates the same concepts (feature extraction, classification, automated response) that a real ML system would use.
RiskPulse · © heyziq05