Security Guide
Tenxyte provides multiple layers of security out of the box.
Table of Contents
- Rate Limiting
- Account Lockout
- Two-Factor Authentication (2FA / TOTP)
- JWT Token Security
- Session & Device Limits
- Password Security
- Security Headers
- CORS
- Audit Logging
- OTP Verification
- Production Checklist
Rate Limiting
Built-in Throttle Classes
Tenxyte ships with pre-configured throttle classes for sensitive endpoints:
| Class | Default Rate | Applied to |
|---|---|---|
LoginThrottle |
5/min | Login endpoints |
LoginHourlyThrottle |
20/hour | Login endpoints |
RegisterThrottle |
3/hour | Register endpoint |
RegisterDailyThrottle |
10/day | Register endpoint |
RefreshTokenThrottle |
30/min | Token refresh |
ProgressiveLoginThrottle |
Progressive | Login endpoints (increases delay after each failure) |
OTPRequestThrottle |
5/hour | OTP request |
OTPVerifyThrottle |
5/min | OTP verification |
PasswordResetThrottle |
3/hour | Password reset request |
PasswordResetDailyThrottle |
10/day | Password reset request |
MagicLinkRequestThrottle |
3/hour | Magic link request |
MagicLinkVerifyThrottle |
10/min | Magic link verification |
Custom URL-Based Throttling
Apply rate limits to any URL without writing a custom class:
# settings.py
TENXYTE_SIMPLE_THROTTLE_RULES = {
'/api/v1/products/': '100/hour',
'/api/v1/search/': '30/min',
'/api/v1/upload/': '5/hour',
'/api/v1/health/$': '1000/min', # $ = exact match
}
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'tenxyte.throttles.SimpleThrottleRule',
],
}
Disable Throttling in Tests
# In your test helpers
from unittest.mock import patch
with patch('rest_framework.throttling.SimpleRateThrottle.allow_request', return_value=True):
response = client.post('/api/v1/auth/login/email/', data)
Account Lockout
After TENXYTE_MAX_LOGIN_ATTEMPTS (default: 5) failed login attempts within TENXYTE_RATE_LIMIT_WINDOW_MINUTES (default: 15 min), the account is locked for TENXYTE_LOCKOUT_DURATION_MINUTES (default: 30 min).
# settings.py
TENXYTE_ACCOUNT_LOCKOUT_ENABLED = True
TENXYTE_MAX_LOGIN_ATTEMPTS = 5
TENXYTE_LOCKOUT_DURATION_MINUTES = 30
TENXYTE_RATE_LIMIT_WINDOW_MINUTES = 15
Locked accounts return 401 with code: 'ACCOUNT_LOCKED'.
Exponential Lockout
When TENXYTE_LOCKOUT_ESCALATION_ENABLED = True (default), each consecutive lockout doubles the duration, capped at TENXYTE_LOCKOUT_MAX_DURATION_MINUTES (default: 1440 = 24h):
| Lockout # | Duration (base=30min) |
|---|---|
| 1st | 30 min |
| 2nd | 60 min |
| 3rd | 120 min |
| 4th | 240 min |
| 5th+ | 1440 min (24h cap) |
The counter resets after a successful login. Formula: min(base × 2^(n-1), max_duration).
Admin unlock via API:
Two-Factor Authentication (2FA / TOTP)
Setup Flow
- User calls
POST /2fa/setup/→ receives QR code + backup codes - User scans QR code with Google Authenticator / Authy
- User calls
POST /2fa/confirm/with first TOTP code → 2FA activated
Login with 2FA
POST /api/v1/auth/login/email/
{
"email": "user@example.com",
"password": "SecurePass123!",
"totp_code": "123456"
}
If totp_code is missing when 2FA is enabled, the response is:
Backup Codes
Generated at setup time (TENXYTE_BACKUP_CODES_COUNT, default: 10).
Each code is single-use. Regenerate with POST /2fa/backup-codes/.
Configuration
TENXYTE_TOTP_ISSUER = 'MyApp' # Name shown in authenticator app
TENXYTE_TOTP_VALID_WINDOW = 1 # Accept ±1 period (30s tolerance)
TENXYTE_BACKUP_CODES_COUNT = 10
JWT Token Security
Algorithm
Tenxyte defaults to HS256 (HMAC-SHA256). For production, switch to an asymmetric algorithm (RS256, EdDSA) to avoid sharing the signing secret across services:
# settings.py
TENXYTE_JWT_ALGORITHM = 'RS256'
TENXYTE_JWT_PRIVATE_KEY = open('/secrets/jwt_private.pem').read()
TENXYTE_JWT_PUBLIC_KEY = open('/secrets/jwt_public.pem').read()
Note: When
HS256is detected, Tenxyte emits aSecurityWarningon service initialization. Use theproductionorenterprisesecure mode preset to automatically switch to RS256.
Access Token Blacklisting
When TENXYTE_TOKEN_BLACKLIST_ENABLED = True (default), access tokens are blacklisted on logout. This prevents token reuse even before expiry.
Refresh Token Rotation
When TENXYTE_REFRESH_TOKEN_ROTATION = True (default), each call to /refresh/ issues a new refresh token and invalidates the old one. This limits the damage from a stolen refresh token.
Short-Lived Access Tokens
Access tokens expire after 15 minutes by default. This limits the exposure window if a token is compromised. Rely on refresh tokens for session persistence:
# Defaults — already secure
TENXYTE_JWT_ACCESS_TOKEN_LIFETIME = 900 # 15 minutes
TENXYTE_JWT_REFRESH_TOKEN_LIFETIME = 604800 # 7 days
GDPR / Data Minimization
Avoid including PII (email, IP address, phone number, etc.) in JWT payloads. JWT tokens are base64-encoded and readable by anyone who intercepts them. Tenxyte emits a SecurityWarning if sensitive keys are detected in extra_claims:
# ❌ Do NOT do this
jwt_service.generate_access_token(user_id, app_id, extra_claims={"email": user.email})
# ✅ Look up by user_id instead
user = User.objects.get(pk=decoded_token.user_id)
Claims that trigger the warning: email, password, phone, phone_number, ip, ip_address, address, ssn, credit_card, dob, date_of_birth.
Required Claims
All issued tokens include exp, iat, and jti. When JWT_ISSUER or JWT_AUDIENCE are configured, iss and aud are also enforced as required claims — tokens missing them are rejected at the PyJWT decoding level.
Key Rotation
Rotate JWT signing keys without invalidating active tokens. Set the previous key so existing tokens can still be verified during the transition:
# 1. Generate a new key
# 2. Move the current key to PREVIOUS
TENXYTE_JWT_PREVIOUS_SECRET_KEY = '<old-key>' # HS256
# or for RS256:
TENXYTE_JWT_PREVIOUS_PUBLIC_KEY = open('/secrets/old_jwt_public.pem').read()
# 3. Set the new key
TENXYTE_JWT_SECRET_KEY = '<new-key>'
New tokens are signed with the current key. On decode, if the primary key fails with InvalidSignatureError, Tenxyte retries with the previous key. Remove PREVIOUS_* once all old tokens have expired.
Cookie-Based Refresh Tokens
Opt-in mode to transport refresh tokens in HttpOnly; Secure; SameSite cookies instead of the JSON body, preventing XSS-based token theft:
TENXYTE_REFRESH_TOKEN_COOKIE_ENABLED = True # Default: False
TENXYTE_REFRESH_TOKEN_COOKIE_NAME = 'tenxyte_refresh'
TENXYTE_REFRESH_TOKEN_COOKIE_SAMESITE = 'Strict'
TENXYTE_REFRESH_TOKEN_COOKIE_PATH = '/api/v1/auth/'
When enabled:
- Login/Refresh responses: refresh_token is removed from the JSON body and set as a Set-Cookie header.
- Refresh requests: The server reads the refresh token from the cookie if the body is empty.
- Logout: The cookie is cleared with max-age=0.
Important: This is opt-in and disabled by default. Clients must handle the absence of
refresh_tokenin JSON responses.
Session & Device Limits
Session Limits
Limit how many concurrent sessions a user can have by rejecting or overriding logins. By default, Tenxyte restricts users to 1 session:
TENXYTE_SESSION_LIMIT_ENABLED = True
TENXYTE_DEFAULT_MAX_SESSIONS = 1 # Overriden by the standard preset to 3
TENXYTE_DEFAULT_SESSION_LIMIT_ACTION = 'revoke_oldest' # or 'deny'
Actions:
- 'revoke_oldest' — revokes the oldest session to make room
- 'deny' — rejects the new login attempt
Per-user override: set user.max_sessions = 5 to override the default for that user.
Device Limits
Limit how many unique devices a user can use by tracking structured device origins. By default, Tenxyte restricts users to 1 device:
TENXYTE_DEVICE_LIMIT_ENABLED = True
TENXYTE_DEFAULT_MAX_DEVICES = 1 # Overriden by the standard preset to 5, high-security to 2
TENXYTE_DEVICE_LIMIT_ACTION = 'deny' # or 'revoke_oldest'
Device identification uses the device_info field sent by the client (structured fingerprint string). Tenxyte uses smart matching — minor version differences (e.g. Chrome 122 vs 123) are treated as the same device.
Device info format (v1):
Build from User-Agent as fallback:
from tenxyte.device_info import build_device_info_from_user_agent
device_info = build_device_info_from_user_agent(request.META.get('HTTP_USER_AGENT', ''))
Password Security
Password Policy
TENXYTE_PASSWORD_MIN_LENGTH = 8
TENXYTE_PASSWORD_MAX_LENGTH = 128
TENXYTE_PASSWORD_REQUIRE_UPPERCASE = True
TENXYTE_PASSWORD_REQUIRE_LOWERCASE = True
TENXYTE_PASSWORD_REQUIRE_DIGIT = True
TENXYTE_PASSWORD_REQUIRE_SPECIAL = True
Password History
Prevent users from reusing recent passwords:
TENXYTE_PASSWORD_HISTORY_ENABLED = True
TENXYTE_PASSWORD_HISTORY_COUNT = 5 # Check against last 5 passwords
Check Password Strength
NIST SP 800-63B Compliance
Accounts without 2FA enabled can be held to a higher minimum password length, per NIST SP 800-63B recommendations:
When set to 15, users without 2FA must use passwords of at least 15 characters. Users with 2FA active continue using the standard PASSWORD_MIN_LENGTH (default: 8).
Security Headers
Add security headers to all responses. By default, Tenxyte provides a highly restrictive set of headers, but they are disabled (False) by default unless you use a secure preset or manually enable them:
TENXYTE_SECURITY_HEADERS_ENABLED = False # Set to True to enable
TENXYTE_SECURITY_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'X-XSS-Protection': '1; mode=block',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'",
'Cross-Origin-Resource-Policy': 'same-origin',
'Cross-Origin-Opener-Policy': 'same-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
}
Add the middleware:
CORS
TENXYTE_CORS_ENABLED = True
TENXYTE_CORS_ALLOWED_ORIGINS = [
'https://yourapp.com',
'http://localhost:3000',
]
TENXYTE_CORS_ALLOW_CREDENTIALS = True
Add the middleware:
Audit Logging
All security-relevant events are automatically logged to the AuditLog model:
| Event | Trigger |
|---|---|
login |
Successful login |
login_failed |
Failed login attempt |
logout |
User logout |
logout_all |
Logout from all devices |
token_refresh |
Access token refreshed |
password_change |
Password changed |
password_reset_request |
Password reset requested |
password_reset_complete |
Password reset completed |
2fa_enabled |
2FA activated |
2fa_disabled |
2FA deactivated |
2fa_backup_used |
2FA Backup Code Used |
account_created |
Account Created |
account_locked |
Account locked after failures |
account_unlocked |
Account Unlocked |
email_verified |
Email Verified |
phone_verified |
Phone Verified |
role_assigned |
Role Assigned |
role_removed |
Role Removed |
permission_changed |
Permission Changed |
app_created |
Application Created |
app_credentials_regenerated |
Application Credentials Regenerated |
account_deleted |
Account Deleted |
suspicious_activity |
Suspicious Activity Detected |
session_limit_exceeded |
Session limit hit |
device_limit_exceeded |
Device limit hit |
new_device_detected |
Login from unrecognized device |
agent_action |
Agent Action Executed |
Query audit logs:
OTP Verification
Email and phone verification use time-limited OTP codes:
TENXYTE_OTP_LENGTH = 6
TENXYTE_OTP_EMAIL_VALIDITY = 15 # minutes
TENXYTE_OTP_PHONE_VALIDITY = 10 # minutes
TENXYTE_OTP_MAX_ATTEMPTS = 5 # before invalidation
Production Checklist
- [ ] Set
TENXYTE_JWT_SECRET_KEYto a strong, unique secret (notSECRET_KEY) - [ ] Switch to
TENXYTE_JWT_ALGORITHM = 'RS256'and configure RSA keys - [ ] Set
TENXYTE_JWT_ACCESS_TOKEN_LIFETIMEto ≤ 900 seconds (15 min) — this is now the default - [ ] Enable
TENXYTE_REFRESH_TOKEN_ROTATION = True - [ ] Enable
TENXYTE_TOKEN_BLACKLIST_ENABLED = True - [ ] Set
TENXYTE_JWT_AUDIENCEto your application identifier - [ ] Enable
TENXYTE_SECURITY_HEADERS_ENABLED = True - [ ] Configure
TENXYTE_CORS_ALLOWED_ORIGINS(never useALLOW_ALL_ORIGINSin production) - [ ] Set
TENXYTE_MAX_LOGIN_ATTEMPTSto a reasonable value (5–10) - [ ] Enable
TENXYTE_PASSWORD_HISTORY_ENABLED = True - [ ] Use HTTPS in production (required for
Strict-Transport-Security) - [ ] Rotate
Applicationsecrets regularly - [ ] Ensure
extra_claimsdo not contain PII (email, IP, phone…) - [ ] Consider enabling cookie refresh token transport for web apps
- [ ] Consider enabling NIST password length for non-MFA accounts (
PASSWORD_MIN_LENGTH_NO_MFA = 15)
OAuth / Social Login Security
PKCE (Proof Key for Code Exchange)
All OAuth providers support PKCE (RFC 7636). Clients should include code_verifier in the code exchange request:
POST /api/v1/auth/social/google/
{
"code": "<authorization-code>",
"redirect_uri": "https://yourapp.com/auth/callback",
"code_verifier": "<PKCE-code-verifier>"
}
The code_verifier is forwarded to the provider's token endpoint. This prevents authorization code interception attacks.
Redirect URI Whitelist
Configure allowed redirect URIs per Application model. When the whitelist is non-empty, any redirect_uri not in the list returns 400 INVALID_REDIRECT_URI:
# Via Django admin or API
app.redirect_uris = [
"https://yourapp.com/auth/callback",
"https://staging.yourapp.com/auth/callback",
]
Configurable OAuth Scopes
Override default scopes per provider:
TENXYTE_SOCIAL_GOOGLE_SCOPES = 'openid email profile'
TENXYTE_SOCIAL_GITHUB_SCOPES = 'read:user user:email'
TENXYTE_SOCIAL_MICROSOFT_SCOPES = 'openid email profile'
TENXYTE_SOCIAL_FACEBOOK_SCOPES = 'email,public_profile'
For the full list of settings with defaults, see Settings Reference.