Authentication classes: choose correctly
DRF supports multiple authentication backends simultaneously, and the one you choose determines your entire security posture. For fintech APIs, here's what works:
Token-based (JWT) — Use djangorestframework-simplejwt for stateless authentication. Configure short-lived access tokens (15 minutes) and longer refresh tokens (7 days) with rotation. This is the standard for mobile and SPA clients. See our JWT security guide for the full list of mistakes to avoid.
Session-based — For admin dashboards and server-rendered views. Django's session framework is mature and handles most things correctly, but you must configure CSRF protection (covered below) and use secure cookie settings.
API keys — For server-to-server integrations (webhooks, partner APIs). Use a library like djangorestframework-api-key. Never use Django's built-in TokenAuthentication for production — tokens don't expire and are stored in plaintext.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
} Permission classes: default to deny
Set IsAuthenticated as the global default, then explicitly loosen permissions on public endpoints. This is the opposite of what many tutorials show, and it's the only approach that prevents accidental unauthenticated access when you add a new viewset and forget to add permissions:
from rest_framework.permissions import IsAuthenticated, AllowAny
class PublicListView(generics.ListAPIView):
permission_classes = [AllowAny] # Explicitly public
class TransferView(generics.CreateAPIView):
permission_classes = [IsAuthenticated] # Default, but explicit is better For fintech, you'll need custom permission classes for resource ownership checks. Without them, you're vulnerable to BOLA/IDOR attacks — any authenticated user can access any other user's records by changing the ID in the URL.
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user Missing object-level permissions on transaction history
A Django-based payment platform used IsAuthenticated but no object-level permission check. Any logged-in user could retrieve any other user's transaction history by iterating through sequential primary keys on the /api/transactions/{id}/ endpoint. Over 400,000 transaction records were accessible.
Throttling: your brute-force defence
DRF's built-in throttling is surprisingly capable. Configure different rates for anonymous and authenticated users, and apply stricter limits to authentication endpoints:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '20/minute',
'user': '100/minute',
'login': '5/minute',
}
} Create a custom throttle scope for login, OTP verification, and password reset endpoints. Without throttling on these endpoints, an attacker can brute-force credentials or enumerate valid OTPs. For deeper patterns, see rate limiting for payment APIs.
Serializer validation: your input firewall
DRF serializers are your primary defence against malformed and malicious input. Define explicit field types, add validators, and never use ModelSerializer without specifying fields explicitly:
class TransferSerializer(serializers.Serializer):
amount = serializers.DecimalField(
max_digits=12, decimal_places=2,
min_value=Decimal('100.00'),
max_value=Decimal('10000000.00')
)
recipient_account = serializers.RegexField(r'^\d{10}$')
bank_code = serializers.CharField(max_length=3, min_length=3)
narration = serializers.CharField(max_length=100, required=False) Never use fields = '__all__' on model serializers — this implicitly exposes every field, including ones you add later. Explicitly list every field you want to accept and return.
CSRF protection for session-based auth
If you use session authentication (for admin panels or server-rendered views), Django's CSRF middleware is critical. DRF enforces CSRF for session auth by default — do not disable it. For JWT-based APIs consumed by mobile apps or SPAs, CSRF is less relevant because the authentication token isn't sent automatically by the browser.
Configure CSRF correctly in settings.py:
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = ['https://app.yourfintech.com'] SQL injection prevention
Django's ORM parameterizes queries automatically. The risk appears when developers use .raw(), .extra(), or direct cursor.execute() calls with string formatting instead of parameter substitution:
# VULNERABLE
User.objects.raw(f"SELECT * FROM users WHERE email = '{email}'")
# SAFE
User.objects.raw("SELECT * FROM users WHERE email = %s", [email]) If you're using raw SQL for complex fintech queries (reporting, reconciliation), always parameterize. Consider using Django's F() expressions and Q() objects to express complex queries without raw SQL whenever possible.
Building a fintech product on Django? We test Django REST APIs for the exact vulnerabilities covered here.
Schedule a Django API pentestProduction hardening
These settings are non-negotiable for any Django fintech application in production:
# settings.py — production
DEBUG = False
ALLOWED_HOSTS = ['api.yourfintech.com']
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY' DEBUG=True in production is a critical vulnerability: it exposes full stack traces, database queries, environment variables, and installed app paths. We find this in roughly one in five Django assessments. Set ALLOWED_HOSTS to your exact production domains — never use ['*'].
Django's security is only as good as your configuration
Django provides excellent security primitives, but DRF adds API-specific concerns that require explicit configuration. Set global defaults to deny access, validate all input through serializers, throttle sensitive endpoints, enforce object-level permissions, and harden your production settings. The framework gives you the tools — you have to use them.
Related reading
Blog: Securing Express APIs · BOLA vulnerabilities · JWT security mistakes
Guides: OWASP for fintech · Web app pentesting · Pentest tools and methodology
Services: API security · Penetration testing · Authentication security