OTP in Django Without Saving it in the Database (Using Redis)
OTP in Django Without Saving it in the Database (Using Redis)
I searched a lot but couldn’t find the ideal way to implement OTP without storing it in a database. So, I will show my own approach.
WARNING: This is my opinion, it could be wrong or vulnerable
1. Installation
Install Required Packages
Recommendation: Use the uv package manager instead of pip.
Installing uv:
1
pip install uv
Create a Virtual Environment and Activate It:
1
2
3
uv venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS/Linux
Install Required Packages:
1
uv pip install django-redis
2. Redis & Docker Configuration
Go to settings.py and add Redis configuration for caching:
1
2
3
4
5
6
7
8
9
10
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://cache:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": REDIS_PASSWORD,
},
}
}
Configure Redis Password in docker-compose.yml
1
2
3
4
5
6
7
8
9
10
cache:
image: redis:6.2-alpine
restart: always
ports:
- '6379:6379'
enviroment:
- REDIS_PASSWORD=$REDIS_PASSWORD
command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD}
volumes:
- cache:/data
3. OTP Generator and Handler
Create otp_handler.py in your utils directory and add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import hashlib
import hmac
import random
from django.conf import settings
from django.core.cache import cache
class OTPHandler:
"""Generate OTP an verify OTP."""
PHONE_SECRET_KEY = settings.PHONE_SECRET_KEY
@staticmethod
def _get_hashed_phone(phone_number):
"""Hash the number."""
return hmac.new(
OTPHandler.PHONE_SECRET_KEY.encode(), phone_number.encode(), hashlib.sha256
).hexdigest()
@staticmethod
def generate_otp(phone_number):
"""Generate Five-digit code and save it in cache."""
hashed_phone = OTPHandler._get_hashed_phone(phone_number)
if cache.get(f"otp_{hashed_phone}"):
return {
"status": "pending",
"message": "OTP already sent. Please wait."
}
otp = str(random.randint(10000,99999)
cache.set(f"otp_{hashed_phone}", otp, timeout=120)
return otp
@staticmethod
def verify_otp(phone_number, otp_code):
"""Get number and OTP and verify it, then delete it from cache."""
hashed_phone = OTPHandler._get_hashed_phone(phone_number)
stored_otp = cache.get(f"otp_{hashed_phone}")
cache.add(f"otp_attempts_{hashed_phone}", 0, timeout=120)
attempts = cache.incr(f"otp_attempts_{hashed_phone}")
if attempts > 3:
return {
"status": "blocked",
"message": "Too many attempts. Try again later.",
}
if stored_otp:
if stored_otp == otp_code:
cache.delete(f"otp_{hashed_phone}")
cache.delete(f"otp_attempts_{hashed_phone}")
return {"status": "valid", "message": "OTP Verified"}
return {"status": "invalid", "message": "Invalid OTP"}
return {
"status": "expired",
"message": "Your OTP has been expired, request for new OTP",
}
Explanation
The OTPHandler class consists of three main parts:
1. _get_hashed_phone(phone_number):
- Hashes the phone number using PHONE_SECRET_KEY to enhance security.
2. generate_otp(phone_number):
Hashes the phone number.
Generates a five-digit OTP.
Stores the OTP in the cache for 120 seconds (2 minutes).
3. verify_otp(phone_number, otp_code):
Retrieves the stored OTP from the cache and compares it with the provided OTP.
Limits OTP attempts to 3 retries.
If successful, deletes the OTP and resets the attempt counter.
Returns a status indicating whether the OTP is valid, invalid, expired, or blocked.
Note: Instead of hashing the phone number, you can store it in plain text if needed.
I hope it helps
This post is licensed under CC BY 4.0 by the author.