User Service - Complete Developer Documentation¶
Service: User Management Service
Port: 8002
Purpose: User signup, OTP verification, password management, feature assignment
Technology: FastAPI (Python 3.9+)
Code Location:/user-service/src/main.py(444 lines, 5 endpoints)
Owner: Backend Team
Last Updated: 2025-12-26
Table of Contents¶
- Service Overview
- Architecture & Responsibilities
- Complete Endpoints
- Signup Flow Deep-Dive
- OTP System
- Email Integration (Azure)
- Password Reset Flow
- Feature Assignment System
- Database Schemas
- Security Analysis
- Deployment
Service Overview¶
The User Service handles all user account management operations except login (which is handled by Auth Service). It's responsible for the complete user lifecycle from signup to subscription management.
Key Responsibilities¶
✅ User Signup - Create new accounts
✅ OTP Generation & Verification - Email-based verification
✅ Email Sending - Azure Communication Services integration
✅ Password Reset - Secure password recovery
✅ Feature Assignment - Assign subscription features to users
✅ User Feature Retrieval - Get user's subscription details
Statistics¶
- Total Lines: 444
- Endpoints: 5 main endpoints
- Dependencies: FastAPI, PyMongo, Azure Communication Email
- Email Provider: Azure Communication Services
- Average Response Time: 100-200ms (2-5 seconds with email sending)
Architecture & Responsibilities¶
Position in System¶
graph TB
subgraph "Client"
A[Frontend]
end
subgraph "Gateway"
B[Gateway<br/>Port 8000]
end
subgraph "Services"
C[Auth Service<br/>Port 8001<br/>LOGIN]
D[User Service<br/>Port 8002<br/>SIGNUP, OTP, RESET]
end
subgraph "External"
E[Azure Communication<br/>Email Service]
end
subgraph "Database"
F[(Cosmos DB)]
end
A -->|POST /v2/signup| B
B --> D
D -->|Send OTP| E
D -->|Create user| F
A -->|POST /v2/verify-otp| B
B --> D
D -->|Update verified| F
D -->|Assign features| F
A -->|POST /v2/login| B
B --> C
C -->|Validate| F
style D fill:#E3F2FD
style C fill:#FFE082
Division from Auth Service¶
| Function | Auth Service (8001) | User Service (8002) |
|---|---|---|
| Login | ✅ Validate & issue JWT | ❌ |
| Signup | ❌ | ✅ Create user + OTP |
| OTP Generation | ❌ | ✅ 6-digit OTP |
| Email Sending | ❌ | ✅ Azure Communication |
| OTP Verification | ❌ | ✅ Verify + activate |
| Password Reset | ❌ | ✅ OTP-based reset |
| Feature Assignment | ❌ | ✅ Free plan features |
| JWT Creation | ✅ Primary | ✅ Also has helper |
Complete Endpoints¶
1. POST /v2/signup¶
Purpose: Create new user account and send OTP for verification
Code Location: Lines 75-131
Request:
Response (Success):
JWT Payload:
2. POST /v2/verify-otp¶
Purpose: Verify OTP and activate user account
Code Location: Lines 138-182
Request:
Response (Success):
3. POST /v2/forgot-password¶
Purpose: Initiate password reset (send OTP)
Code Location: Lines 241-260
Request:
Response:
4. POST /v2/reset-password¶
Purpose: Reset password after OTP verification
Code Location: Lines 263-283
Request:
async def reset_password(
email: str = Form(...),
otp: str = Form(...),
new_password: str = Form(...)
)
Response:
5. GET /v2/get-user-features/{user_id}¶
Purpose: Get user's subscription features
Code Location: Lines 286-422
Request:
Response:
{
"user_features": {
"user_id": "User-123456",
"subscription_id": "sub_009",
"no_of_chatbots": 1,
"no_of_chat_sessions": 50,
"no_of_linkcrawls": "50",
"no_of_users": "1",
"grace_period": "14",
"features": [...]
}
}
Signup Flow Deep-Dive¶
Complete Signup Process¶
sequenceDiagram
participant C as Client
participant U as User Service
participant DB as Cosmos DB
participant AZ as Azure Email
C->>U: POST /v2/signup<br/>(email, password, name)
Note over U: Step 1: Check existing user
U->>DB: findOne({email})
DB-->>U: User or null
alt User exists & verified
U-->>C: 400 "Email already registered"
end
alt User exists & unverified
Note over U: Step 2a: Resend OTP
U->>U: generate_otp() → "123456"
U->>AZ: Send OTP email
AZ-->>U: Email sent
U->>DB: Update OTP & expiry
U-->>C: 200 {token}
end
alt New user
Note over U: Step 2b: Create user
U->>U: generate_otp() → "654321"
U->>AZ: Send OTP email
AZ-->>U: Email sent
U->>DB: insertOne (user with OTP)
U-->>C: 200 {token}
end
Step 1: Check Existing User¶
Code (Lines 83-103):
existing_user = users_collection.find_one({"email": email})
if existing_user:
# User already exists
if existing_user.get("verified", False):
# Already verified → Cannot signup again
raise HTTPException(status_code=400, detail="Email already registered")
# Unverified → Resend OTP
otp = generate_otp() # New 6-digit OTP
expiration_time = time.time() + 60 # 60 seconds
if send_otp_email(email, otp):
users_collection.update_one(
{"email": email},
{"$set": {"otp": otp, "otp_expiration": expiration_time}}
)
return {"token": create_jwt_token({"email": email, "otp_sent": True})}
else:
raise HTTPException(status_code=500, detail="Failed to send OTP")
Database Query:
Possible Results:
- Null → New user, proceed to create
- User with
verified: true→ Error 400 - User with
verified: false→ Resend OTP
Step 2a: Resend OTP (Unverified User)¶
Scenario: User signed up but never verified, trying to signup again
Flow:
- Generate new OTP:
generate_otp()→"123456" - Send email via Azure
- Update existing document with new OTP
- Return JWT token
Database Operation:
db.users_multichatbot_v2.updateOne(
{ email: "user@example.com" },
{
$set: {
otp: "123456",
otp_expiration: 1735214460, // Current timestamp + 60
},
}
);
Step 2b: Create New User¶
Code (Lines 105-131):
otp = generate_otp() # "654321"
expiration_time = time.time() + 60 # Unix timestamp
current_date = datetime.now().strftime("%Y-%m-%d")
if send_otp_email(email, otp):
users_collection.insert_one({
"email": email,
"password": password, # ⚠️ PLAIN TEXT!
"name": name,
"otp": otp,
"otp_expiration": expiration_time,
"verified": False,
"account_status": "paused", # Not active yet
"payment_status": "pending",
"user_created_at(DATE)": current_date,
"subscription_id": "sub_009", # Free plan
"subscription_date": current_date
})
return {"token": create_jwt_token({"email": email, "otp_sent": True})}
New User Document:
{
"_id": ObjectId("65a1b2c3d4e5f6789abc"),
"email": "newuser@example.com",
"password": "mypassword123", // ⚠️ PLAIN TEXT - SECURITY ISSUE
"name": "New User",
"otp": "654321",
"otp_expiration": 1735214460, // Unix timestamp (60 seconds from now)
"verified": false,
"account_status": "paused", // Will be "active" after verification
"payment_status": "pending",
"user_created_at(DATE)": "2025-01-15",
"subscription_id": "sub_009", // Free plan
"subscription_date": "2025-01-15",
// Note: user_id NOT set yet (set during OTP verification)
}
⚠️ CRITICAL SECURITY ISSUE:
Passwords stored in plain text - same issue as Auth Service!
OTP Generation¶
Code (Lines 134-135):
Example Output: "123456", "987654", "000000", etc.
Characteristics:
- Length: 6 digits
- Character Set: 0-9
- Total Combinations: 1,000,000 (weak but acceptable for 60-second expiry)
Security Note:
With 1 million combinations and 60-second expiry, brute force is difficult but not impossible. Consider:
- Adding rate limiting (e.g., 3 attempts per minute)
- Lockout after 5 failed attempts
OTP System¶
Email Sending (Azure Communication Services)¶
Code Location: /user-service/src/utils/email_utils.py
Configuration (Lines 47-49):
EMAIL_ENDPOINT = "https://mailing-sevice.india.communication.azure.com/"
EMAIL_ACCESS_KEY = "CgdEWi6fBCJv4c0EHOkq6ZUSML0VQSG49qXgEfrtlLmXY76HV7DeJQQJ99BBACULyCplbknJAAAAAZCSbc3T"
SENDER_EMAIL = "DoNotReply@machineagents.ai"
email_client = EmailClient(endpoint=EMAIL_ENDPOINT, credential=EMAIL_ACCESS_KEY)
⚠️ SECURITY ISSUE: Hardcoded credentials! Should use environment variables.
Send OTP Email Function¶
Code (Lines 64-79):
def send_otp_email(email: str, otp: str):
try:
message = {
"senderAddress": SENDER_EMAIL, # DoNotReply@machineagents.ai
"recipients": {"to": [{"address": email}]},
"content": {
"subject": "Your OTP Code",
"plainText": f"Your OTP is {otp}. It is valid for 60 seconds."
}
}
email_client.begin_send(message)
_log_json("SUCCESS", "OTP email sent successfully", email_recipient=email)
return True
except Exception as e:
_log_json("ERROR", "Failed to send OTP email",
email_recipient=email,
exception_type=type(e).__name__,
exception_message=str(e),
stack_trace=traceback.format_exc())
return False
Email Content¶
Subject: Your OTP Code
Body (Plain Text):
Improvement Suggestion:
Welcome to MachineAvatars!
Your verification code is: 123456
This code will expire in 60 seconds.
If you did not request this code, please ignore this email.
---
MachineAvatars Team
https://machineavatars.com
OTP Expiration¶
Expiry Time: 60 seconds
Calculation (Line 92, 107):
Validation (Line 147):
if time.time() > user.get("otp_expiration", 0):
raise HTTPException(status_code=400, detail="OTP expired")
Example:
- OTP Created: 14:00:00 (timestamp: 1735214400)
- Expiry: 14:01:00 (timestamp: 1735214460)
- User tries at 14:01:30: ❌ Expired!
Email Integration (Azure)¶
Azure Communication Services Setup¶
Service: Azure Communication Email
Region: India (india.communication.azure.com)
Sender Domain: machineagents.ai
Python Client:
from azure.communication.email import EmailClient
email_client = EmailClient(
endpoint="https://mailing-sevice.india.communication.azure.com/",
credential="CgdEWi6fBCJv4..." # Access Key
)
Email Message Structure¶
Azure API Format:
message = {
"senderAddress": "DoNotReply@machineagents.ai",
"recipients": {
"to": [
{"address": "user@example.com"}
]
},
"content": {
"subject": "Your OTP Code",
"plainText": "Your OTP is 123456. It is valid for 60 seconds."
# Could also include "html" for rich emails
}
}
email_client.begin_send(message)
Email Delivery¶
Method: begin_send() - Asynchronous
Typical Delivery Time: 1-5 seconds
Retry Logic: None (returns True/False immediately)
Improvement:
Logging¶
Success Log:
{
"timestamp": "2025-01-15T14:00:00Z",
"level": "SUCCESS",
"message": "OTP email sent successfully",
"service_name": "email-service",
"email_recipient": "user@example.com"
}
Error Log:
{
"timestamp": "2025-01-15T14:00:00Z",
"level": "ERROR",
"message": "Failed to send OTP email",
"service_name": "email-service",
"email_recipient": "user@example.com",
"exception_type": "HttpResponseError",
"exception_message": "Authentication failed",
"stack_trace": "Traceback..."
}
OTP Verification Flow¶
Endpoint: POST /v2/verify-otp¶
Code (Lines 138-182):
@app.post("/v2/verify-otp")
async def verify_otp(email: str = Form(...), otp: str = Form(...)):
user = users_collection.find_one({"email": email})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check expiration
if time.time() > user.get("otp_expiration", 0):
raise HTTPException(status_code=400, detail="OTP expired")
# Validate OTP
if user["otp"] == otp:
# Generate or use existing user_id
existing_user_id = user.get("user_id")
if existing_user_id:
user_id = existing_user_id
else:
user_id = f"User-{random.randint(100000, 999999)}"
# Mark user as verified and active
users_collection.update_one(
{"email": email},
{"$set": {
"verified": True,
"user_id": user_id,
"account_status": "active" # Activate account
}}
)
# Assign free plan features
await assign_free_plan_features(user_id)
return {"message": "OTP verified successfully", "user_id": user_id}
raise HTTPException(status_code=400, detail="Invalid OTP")
Flow Diagram¶
sequenceDiagram
participant C as Client
participant U as User Service
participant DB as Cosmos DB
C->>U: POST /v2/verify-otp<br/>(email, otp)
U->>DB: findOne({email})
DB-->>U: User document
alt User not found
U-->>C: 404 "User not found"
end
alt OTP expired
Note over U: time.time() > otp_expiration
U-->>C: 400 "OTP expired"
end
alt OTP invalid
Note over U: user["otp"] != otp
U-->>C: 400 "Invalid OTP"
end
alt OTP valid
Note over U: Generate user_id if needed
U->>DB: updateOne (verified=true, user_id, active)
Note over U: Step 2: Assign features
U->>U: assign_free_plan_features(user_id)
U->>DB: Insert features_per_user
U-->>C: 200 {message, user_id}
end
User ID Generation¶
Code (Lines 154-159):
existing_user_id = user.get("user_id")
if existing_user_id:
user_id = existing_user_id # Already has ID (edge case)
else:
user_id = f"User-{random.randint(100000, 999999)}"
# Example: "User-123456", "User-987654"
Format: User-XXXXXX (6 random digits)
Collision Probability: Low but possible (1 in 1 million)
Better Approach:
import time
user_id = f"User-{int(time.time())}_{random.randint(100, 999)}"
# Example: "User-1735214400_456"
Account Activation¶
Database Update:
db.users_multichatbot_v2.updateOne(
{ email: "user@example.com" },
{
$set: {
verified: true,
user_id: "User-123456",
account_status: "active", // Was "paused" during signup
},
}
);
Before:
{
"email": "user@example.com",
"verified": false,
"account_status": "paused",
"user_id": undefined // Not set
}
After:
{
"email": "user@example.com",
"verified": true,
"account_status": "active",
"user_id": "User-123456"
}
Feature Assignment System¶
Free Plan Assignment¶
Code (Lines 185-237):
Trigger: After successful OTP verification
Function: assign_free_plan_features(user_id)
Step-by-Step Process¶
1. Get Subscription Details
subscription_id = "sub_009" # Free plan ID
subscription = db["subscriptions"].findOne({"subscription_id": subscription_id})
Subscription Document:
{
"_id": ObjectId("..."),
"subscription_id": "sub_009",
"name": "Free Plan",
"pricing": 0,
"offers": "Basic features for trying out",
"featureIDs": ["feat_001", "feat_002"], // Feature IDs included
"no_of_chatbots": 1,
"no_of_chat_sessions": 50,
"no_of_linkcrawls": "50",
"no_of_users": "1",
"grace_period": "14"
}
2. Get Base Features
Base Features Document:
{
"_id": ObjectId("..."),
"subscription_id": "sub_009",
"baseFeatureID": "base_009",
"no_of_chatbots": 1,
"no_of_chat_sessions": 50,
"no_of_linkcrawls": "50",
"no_of_users": "1",
"grace_period": "14"
}
3. Parse Feature IDs
Code (Lines 205-215):
feature_ids = subscription.get("featureIDs", [])
# Handle both list and comma-separated string formats
if not isinstance(feature_ids, list):
if isinstance(feature_ids, str) and feature_ids:
feature_ids = [fid.strip() for fid in feature_ids.split(",") if fid.strip()]
else:
feature_ids = []
Examples:
- List:
["feat_001", "feat_002"]→["feat_001", "feat_002"] - String:
"feat_001, feat_002"→["feat_001", "feat_002"] - Empty:
""→[]
4. Create User Features Record
Code (Lines 218-231):
user_features = {
"user_id": user_id,
"subscription_id": subscription_id,
"baseFeatureID": base_features.get("baseFeatureID", "base_009"),
"featureIDs": feature_ids,
"no_of_chatbots": base_features.get("no_of_chatbots", 1),
"no_of_chat_sessions": base_features.get("no_of_chat_sessions", 50),
"no_of_linkcrawls": base_features.get("no_of_linkcrawls", "50"),
"no_of_users": base_features.get("no_of_users", "1"),
"grace_period": base_features.get("grace_period", "14")
}
features_per_user_collection.insert_one(user_features)
Inserted Document:
// Collection: features_per_user
{
"_id": ObjectId("..."),
"user_id": "User-123456",
"subscription_id": "sub_009",
"baseFeatureID": "base_009",
"featureIDs": ["feat_001", "feat_002"],
"no_of_chatbots": 1, // Can create 1 chatbot
"no_of_chat_sessions": 50, // 50 conversations/month
"no_of_linkcrawls": "50", // Can crawl 50 URLs
"no_of_users": "1", // Single user (no team)
"grace_period": "14" // 14 days grace period
}
Free Plan Limits¶
| Limit | Value | Description |
|---|---|---|
| Chatbots | 1 | Can create 1 chatbot |
| Chat Sessions | 50 | 50 conversations per month |
| Link Crawls | 50 | Can crawl 50 web pages |
| Users | 1 | Single user (no team members) |
| Grace Period | 14 days | Time before account restrictions |
Password Reset Flow¶
Step 1: Forgot Password¶
Endpoint: POST /v2/forgot-password
Code (Lines 241-260):
@app.post("/v2/forgot-password")
async def forgot_password(email: str = Form(...)):
user = users_collection.find_one({"email": email})
if not user:
raise HTTPException(status_code=404, detail="Email not registered")
otp = generate_otp()
expiration_time = time.time() + 60
if send_otp_email(email, otp):
users_collection.update_one(
{"email": email},
{"$set": {"otp": otp, "otp_expiration": expiration_time}}
)
return {"token": create_jwt_token({"email": email, "otp_sent": True})}
else:
raise HTTPException(status_code=500, detail="Failed to send OTP")
Flow:
- Check if email exists
- Generate new OTP (60-second expiry)
- Send OTP via email
- Update user document with OTP
- Return JWT token
Step 2: Reset Password¶
Endpoint: POST /v2/reset-password
Code (Lines 263-283):
@app.post("/v2/reset-password")
async def reset_password(
email: str = Form(...),
otp: str = Form(...),
new_password: str = Form(...)
):
user = users_collection.find_one({"email": email})
if not user:
raise HTTPException(status_code=404, detail="User not found")
if time.time() > user.get("otp_expiration", 0):
raise HTTPException(status_code=400, detail="OTP expired")
if user["otp"] == otp:
# Update password (PLAIN TEXT!)
users_collection.update_one(
{"email": email},
{"$set": {"password": new_password}}
)
return {"token": create_jwt_token({"email": email, "password_reset": True})}
raise HTTPException(status_code=400, detail="Invalid OTP")
⚠️ SECURITY ISSUE: Password updated as plain text (Line 278)!
Complete Reset Flow¶
sequenceDiagram
participant C as Client
participant U as User Service
participant DB as Cosmos DB
participant AZ as Azure Email
Note over C,AZ: STEP 1: Forgot Password
C->>U: POST /v2/forgot-password<br/>(email)
U->>DB: findOne({email})
DB-->>U: User found
U->>U: generate_otp()
U->>AZ: Send OTP email
AZ-->>U: Email sent
U->>DB: Update OTP
U-->>C: {token}
Note over C: User checks email, gets OTP
Note over C,AZ: STEP 2: Reset Password
C->>U: POST /v2/reset-password<br/>(email, otp, new_password)
U->>DB: findOne({email})
DB-->>U: User with OTP
alt OTP valid
U->>DB: updateOne (password = new_password)
U-->>C: {token}
else OTP invalid/expired
U-->>C: 400 Error
end
Get User Features¶
Endpoint: GET /v2/get-user-features/{user_id}¶
Code (Lines 286-422):
Purpose: Retrieve complete feature set for a user by aggregating 4 collections
Collections Queried¶
- features_per_user - User's assigned features
- subscriptions - Subscription details (pricing, offers)
- baseFeatures - Base limits for subscription
- featuresGlobal - Individual feature details
Data Aggregation Flow¶
graph TB
A[GET /v2/get-user-features/User-123456] --> B[Query features_per_user]
B --> C{Found?}
C -->|No| D[Return 404]
C -->|Yes| E[Get subscription_id]
E --> F[Query subscriptions collection]
F --> G{Found in subscriptions?}
G -->|No| H[Query roles collection<br/>Fallback]
H --> I[Convert role to subscription format]
G -->|Yes| J[Get baseFeatureID]
I --> J
J --> K[Query baseFeatures]
K --> L[Get featureIDs]
L --> M[Query featuresGlobal<br/>Using $in operator]
M --> N[Aggregate all data]
N --> O[Return merged response]
Response Structure¶
{
"user_features": {
"user_id": "User-123456",
"subscription_id": "sub_009",
"baseFeatureID": "base_009",
"featureIDs": ["feat_001", "feat_002"],
// User limits (priority: user > base > subscription)
"no_of_chatbots": 1,
"no_of_chat_sessions": 50,
"no_of_linkcrawls": "50",
"no_of_users": "1",
"grace_period": "14",
// Subscription details
"subscription_details": {
"pricing": 0,
"offers": "Basic features for trying out"
},
// Base feature details
"base_feature_details": {
"no_of_chatbots": 1,
"no_of_chat_sessions": 50,
"no_of_users": "1",
"no_of_linkcrawls": "50",
"grace_period": "14"
},
// Individual features
"features": [
{
"featureID": "feat_001",
"feature": "3D Avatar Support",
"feature_value": "true"
},
{
"featureID": "feat_002",
"feature": "Basic Analytics",
"feature_value": "enabled"
}
]
}
}
Priority System¶
Code (Lines 389-393):
"no_of_chatbots": (
user_features.get("no_of_chatbots") or
base_feature.get("no_of_chatbots") or
subscription.get("no_of_chatbots", "1")
)
Priority Order:
- User-specific override (features_per_user)
- Base features (baseFeatures collection)
- Subscription default (subscriptions collection)
- Hardcoded fallback ("1")
Database Schemas¶
1. users_multichatbot_v2¶
Complete Schema:
{
_id: ObjectId("..."),
// Identity
email: "user@example.com", // Unique, indexed
user_id: "User-123456", // Generated during OTP verification
name: "John Doe",
// Authentication
password: "plaintext123", // ⚠️ PLAIN TEXT!
verified: true, // Email verified via OTP
// OTP System
otp: "123456", // Current OTP (or last used)
otp_expiration: 1735214460, // Unix timestamp (60 seconds)
// Account Status
account_status: "active", // "paused" or "active"
payment_status: "pending", // "pending", "paid", "failed"
// Subscription
subscription_id: "sub_009", // Free plan
subscription_date: "2025-01-15",
// Timestamps
user_created_at(DATE): "2025-01-15", // Note: Unusual field name with ()
last_login: "2025-01-20T14:00:00Z" // Optional
}
2. features_per_user¶
Schema:
{
_id: ObjectId("..."),
user_id: "User-123456",
subscription_id: "sub_009",
baseFeatureID: "base_009",
featureIDs: ["feat_001", "feat_002"],
// Limits
no_of_chatbots: 1,
no_of_chat_sessions: 50,
no_of_linkcrawls: "50", // Note: String
no_of_users: "1", // Note: String
grace_period: "14" // Days
}
3. subscriptions¶
Schema:
{
_id: ObjectId("..."),
subscription_id: "sub_009",
name: "Free Plan",
pricing: 0,
offers: "Basic features for trying out",
baseFeatureID: "base_009",
featureIDs: ["feat_001", "feat_002"], // Can be string: "feat_001,feat_002"
// Limits
no_of_chatbots: 1,
no_of_chat_sessions: 50,
no_of_linkcrawls: "50",
no_of_users: "1",
grace_period: "14"
}
4. baseFeatures¶
Schema:
{
_id: ObjectId("..."),
subscription_id: "sub_009",
baseFeatureID: "base_009",
// Limits
no_of_chatbots: 1,
no_of_chat_sessions: 50,
no_of_linkcrawls: "50",
no_of_users: "1",
grace_period: "14"
}
5. featuresGlobal¶
Schema:
{
_id: ObjectId("..."),
featureID: "feat_001",
feature: "3D Avatar Support",
feature_value: "true", // or "enabled", numeric, etc.
description: "Allows users to use 3D avatars in chatbots"
}
6. roles (Fallback)¶
Schema (Lines 327-340):
{
_id: ObjectId("..."),
role_id: "role_admin", // Used as subscription_id fallback
baseFeatureID: "base_admin",
featureIDs: [...],
no_of_chatbots: 999,
no_of_chat_sessions: 999999,
// ... etc
}
Note: Roles collection used as fallback if subscription not found (Lines 324-343)
Security Analysis¶
Critical Issues¶
1. ⚠️ PLAIN TEXT PASSWORDS
Locations:
- Line 115:
insertOne({"password": password}) - Line 278:
updateOne({"$set": {"password": new_password}})
Impact:
- Same as Auth Service - all passwords exposed if DB breached
- Violates GDPR, OWASP, PCI-DSS
Fix:
import bcrypt
# Signup
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
users_collection.insert_one({"password_hash": password_hash.decode()})
# Reset
new_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt(rounds=12))
users_collection.update_one({"email": email}, {"$set": {"password_hash": new_hash.decode()}})
2. ⚠️ HARDCODED EMAIL CREDENTIALS
Location: /user-service/src/utils/email_utils.py (Lines 47-48)
Problem:
EMAIL_ENDPOINT = "https://mailing-sevice.india.communication.azure.com/"
EMAIL_ACCESS_KEY = "CgdEWi6fBCJv4..." # Hardcoded!
Fix:
EMAIL_ENDPOINT = os.getenv("AZURE_EMAIL_ENDPOINT")
EMAIL_ACCESS_KEY = os.getenv("AZURE_EMAIL_ACCESS_KEY")
SENDER_EMAIL = os.getenv("SENDER_EMAIL", "DoNotReply@machineagents.ai")
if not EMAIL_ENDPOINT or not EMAIL_ACCESS_KEY:
raise ValueError("Email credentials must be set in environment")
3. ⚠️ NO RATE LIMITING
Problem: Unlimited OTP requests possible
Attack Scenarios:
- Email bombing: Request OTP repeatedly to spam user's inbox
- Cost attack: Generate Azure email costs
- Brute force: Try all 1 million OTP combinations (unlikely but possible)
Fix:
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@app.post("/v2/signup")
@limiter.limit("3/minute") # 3 signups per minute
async def signup(...):
...
@app.post("/v2/verify-otp")
@limiter.limit("5/minute") # 5 OTP attempts per minute
async def verify_otp(...):
...
4. ⚠️ NO ACCOUNT LOCKOUT
Problem: No protection after multiple failed OTP attempts
Fix:
Code:
if user.get("failed_otp_attempts", 0) >= 5:
if user.get("account_locked_until", 0) > time.time():
raise HTTPException(status_code=403, detail="Account temporarily locked")
if user["otp"] != otp:
users_collection.update_one(
{"email": email},
{"$inc": {"failed_otp_attempts": 1}}
)
if user.get("failed_otp_attempts", 0) + 1 >= 5:
users_collection.update_one(
{"email": email},
{"$set": {"account_locked_until": time.time() + 900}} # 15 min lockout
)
raise HTTPException(status_code=400, detail="Invalid OTP")
5. ⚠️ PREDICTABLE USER IDs
Problem (Line 158):
user_id = f"User-{random.randint(100000, 999999)}"
# Only 1 million possibilities, collisions possible
Fix:
6. ⚠️ CORS ALLOWS ALL ORIGINS
Location: Lines 24-30
Same as Auth Service - should restrict to specific domains
Security Best Practices Implemented¶
✅ OTP Expiration: 60-second time limit
✅ Email Verification: Required before account activation
✅ HTTPS: Enforced in production
✅ Logging: Comprehensive JSON logging
✅ Error Messages: Generic (though could be better)
Deployment¶
Docker Configuration¶
Dockerfile:
FROM python:3.9-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY src/ .
EXPOSE 8002
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
Docker Compose¶
user-service:
build: ./user-service
container_name: user-service
ports:
- "8002:8002"
networks:
- app-network
labels:
- "logging=loki"
- "app=machine-agent-app"
environment:
- MONGO_URI=${MONGO_URI}
- MONGO_DB_NAME=Machine_agent_demo
- JWT_SECRET=${JWT_SECRET}
- JWT_ALGORITHM=HS256
# Azure Email (should be added)
- AZURE_EMAIL_ENDPOINT=${AZURE_EMAIL_ENDPOINT}
- AZURE_EMAIL_ACCESS_KEY=${AZURE_EMAIL_ACCESS_KEY}
# DataDog APM
- DD_SERVICE=user-service
- DD_ENV=production
- DD_VERSION=1.0.0
- DD_TRACE_ENABLED=true
restart: always
Requirements.txt¶
# Web framework
fastapi>=0.95.0
uvicorn[standard]>=0.22.0
# Database
pymongo>=4.3.3
# Authentication & Security
PyJWT>=2.8.0
python-jose>=3.3.0
python-multipart>=0.0.6
# Environment variables
python-dotenv>=1.0.0
# Azure Email
azure-communication-email>=1.0.0
# Monitoring
ddtrace>=1.19.0
Performance Metrics¶
| Operation | Latency | Notes |
|---|---|---|
| Signup (without email) | 50-100ms | MongoDB insert |
| Signup (with email) | 2-5 seconds | Azure email API |
| Verify OTP | 100-200ms | MongoDB + feature assignment |
| Forgot Password | 2-5 seconds | MongoDB + email |
| Reset Password | 50-100ms | MongoDB update |
| Get User Features | 150-300ms | 4 collection queries + aggregation |
Bottleneck: Azure email sending (2-5 seconds)
Related Documentation¶
- Auth Service Documentation - Login flow
- Gateway Service Documentation - How requests are routed
- System Architecture
Recommendations & Next Steps¶
Critical (Security)¶
- ⚠️ IMPLEMENT PASSWORD HASHING (bcrypt) - URGENT!
- ⚠️ Move Email Credentials to Environment Variables
- ⚠️ Add Rate Limiting (slowapi)
- ⚠️ Improve User ID Generation (UUID-based)
Improvements¶
- Add Account Lockout - After 5 failed OTP attempts
- Enhance Email Template - Branded HTML emails
- Add Email Retry Logic - Handle Azure failures
- Improve Error Messages - More user-friendly
Code Quality¶
- Fix Field Name:
user_created_at(DATE)→created_at - Consistent Types:
no_of_*should be integers, not strings - Add Unit Tests - Test OTP generation, expiration, etc.
- Add Input Validation - Email format, password strength
Last Updated: 2025-12-26
Code Version: user-service/src/main.py (444 lines)
Total Endpoints: 5
Review Cycle: Monthly (Critical Security Service)
"Where users become users - one OTP at a time."