A zero-database OTP authentication service built in Python. Works as an embedded library or a standalone REST API that any tech stack can call.
OTP = HMAC-SHA256(server_secret + identity + time_window)[:6 digits]
- No database writes — the OTP is derived deterministically from the server secret and the current 5-minute time window.
- Verification re-derives the same value and does a constant-time comparison.
- Clock skew is handled by checking ±1 adjacent windows (
drift_tolerance). - A signed JWT is issued after successful verification — also stateless.
stateless-otp/
├── core/
│ ├── otp.py # OTPEngine — stateless HMAC-TOTP generation + verification
│ ├── token.py # TokenIssuer — HS256 JWT issuer/verifier (no PyJWT dependency)
│ ├── ratelimit.py # RateLimiter — sliding-window abuse prevention
│ ├── delivery.py # Pluggable backends: Console, SMTP, Twilio, Webhook
│ └── service.py # OTPService — high-level orchestrator
├── api/
│ ├── main.py # FastAPI app + config from env vars
│ ├── routes.py # REST endpoints
│ └── schemas.py # Pydantic request/response models
├── sdk/
│ ├── javascript/ # Browser + Node.js SDK (zero dependencies)
│ └── python/ # Python HTTP client SDK (stdlib only)
├── tests/
│ ├── test_otp.py # 27 unit tests (engine, token, rate limiter, service)
│ └── test_api.py # 12 integration tests (FastAPI endpoints)
├── .env.example # All configurable environment variables
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
pip install -r requirements.txt# OTP secret
python -c "from core.otp import generate_secret_key; print(generate_secret_key())"
# JWT secret
python -c "import secrets; print(secrets.token_hex(32))"cp .env.example .env
# Edit .env with your secrets and delivery backenduvicorn api.main:app --reload
# → http://localhost:8000/docs (Swagger UI)pytest tests/ -vRequest an OTP to be sent to the user.
// Request
{ "identity": "[email protected]" }
// Response 200
{ "success": true, "message": "OTP sent successfully.", "ttl_seconds": 247 }Verify the OTP and receive a JWT access token.
// Request
{ "identity": "[email protected]", "otp": "482931" }
// Response 200
{
"success": true,
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 900
}Validate a JWT and return its claims.
// Request
{ "token": "eyJ..." }
// Response 200
{ "active": true, "claims": { "sub": "[email protected]", "exp": 1234567890 } }Verify via Authorization: Bearer <token> header (nginx auth_request compatible).
No HTTP overhead — use OTPService directly in Django, Flask, FastAPI, etc.
from core.service import OTPService, OTPServiceConfig
from core.delivery import SMTPBackend, SMTPConfig
svc = OTPService(
config=OTPServiceConfig(
otp_secret_key=b"your-32-byte-secret",
jwt_secret="your-jwt-secret",
),
delivery=SMTPBackend(SMTPConfig(
host="smtp.gmail.com",
username="[email protected]",
password="app-password",
from_address="[email protected]",
)),
)
# Step 1 — send OTP
svc.request_otp("[email protected]")
# Step 2 — verify
result = svc.verify_otp("[email protected]", "482931")
if result.success:
print(result.access_token) # use this JWTconst { OTPClient } = require('./sdk/javascript/stateless-otp-sdk');
const client = new OTPClient({ baseUrl: 'http://localhost:8000/api/v1' });
// Step 1
await client.requestOTP('[email protected]');
// Step 2
const { accessToken } = await client.verifyOTP('[email protected]', userInputOTP);
// Token is auto-stored on the client for subsequent requests
const { active, claims } = await client.verifyBearer();| Variable | Default | Description |
|---|---|---|
OTP_SECRET_KEY |
(required) | Base64 HMAC secret — run generate_secret_key() |
JWT_SECRET |
(required) | HS256 JWT signing secret |
OTP_DIGITS |
6 |
OTP length (4–10) |
OTP_WINDOW_SECONDS |
300 |
Time window per OTP (seconds) |
OTP_DRIFT_TOLERANCE |
1 |
Adjacent windows to accept |
ACCESS_TOKEN_TTL |
900 |
JWT lifetime (seconds) |
DELIVERY_BACKEND |
console |
console / smtp / twilio |
SMTP_HOST |
— | SMTP server hostname |
TWILIO_ACCOUNT_SID |
— | Twilio account SID |
RATE_REQUEST_OTP_MAX |
5 |
Max OTP sends per 10 min |
RATE_VERIFY_OTP_MAX |
10 |
Max verify attempts per 10 min |
# Build and run
docker-compose up --build
# With your own secrets
OTP_SECRET_KEY=xxx JWT_SECRET=yyy DELIVERY_BACKEND=smtp \
SMTP_HOST=smtp.gmail.com [email protected] \
SMTP_PASSWORD=pw [email protected] \
docker-compose upOTP_SECRET_KEYmust stay server-side only. Rotating it invalidates all current OTPs.- Use HTTPS in production — OTPs are single-use but travel in plaintext over HTTP.
- Rate limiting defaults block 5 OTP requests and 10 verify attempts per 10-minute window per identity.
- For multi-process deployments, swap the in-memory
RateLimiterwith a Redis backend by subclassingRateLimiter. - The JWT verifier uses constant-time comparison (
hmac.compare_digest) to prevent timing attacks.