A simple authentication module for django rest framework
Project description
🔒 DRF Authentify
Modern token authentication for Django Rest Framework with multi-device support, auto-refresh, and session context.
Why Choose DRF Authentify?
DRF Authentify reimagines token authentication for modern applications. Unlike DRF's default token system, it provides:
- Multi-device sessions - Users stay logged in across mobile, web, and desktop simultaneously
- Session context - Store device info, IP addresses, and custom metadata with each token
- Auto-refresh - Tokens renew automatically during active use
- Flexible security - Choose between single-login enforcement or multiple active sessions
- Production-ready - Secure token hashing, expiration management, and audit trails
Installation
pip install drf-authentify
Requirements: Python ≥ 3.9, Django ≥ 3.2, Django REST Framework ≥ 3.0
Quick Start
1. Add to Your Project
# settings.py
INSTALLED_APPS = [
# ... your apps
'drf_authentify',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'drf_authentify.auth.AuthorizationHeaderAuthentication',
'drf_authentify.auth.CookieAuthentication',
],
}
2. Run Migrations
python manage.py migrate
3. Create Your First Token
from drf_authentify.services import TokenService
# In your login view
token_set = TokenService.generate_header_token(
user=request.user,
context={
"device": "mobile",
"ip_address": request.META.get('REMOTE_ADDR')
}
)
# Return to client
return Response({
'access_token': token_set.access_token,
'refresh_token': token_set.refresh_token,
})
Your API is now protected! Clients authenticate by sending:
Authorization: Bearer <access_token>
Core Concepts
Multi-Device Authentication
Users can maintain multiple active sessions across different devices. Each token stores its own context:
# Mobile login
mobile_token = TokenService.generate_header_token(
user=user,
context={"device": "iPhone", "app_version": "2.1"}
)
# Web login (doesn't invalidate mobile token)
web_token = TokenService.generate_header_token(
user=user,
context={"device": "Chrome", "browser_version": "120"}
)
To enforce single-device login instead:
# settings.py
DRF_AUTHENTIFY = {
'ENFORCE_SINGLE_LOGIN': True,
}
Session Context
Store custom metadata with each token for authorization decisions:
token_set = TokenService.generate_header_token(
user=user,
context={
"device_id": "abc-123",
"location": "US",
"beta_features": True,
"subscription_tier": "premium"
}
)
# Access in your views
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def premium_feature(request):
if not request.auth.context_obj.beta_features:
return Response({'error': 'Beta access required'}, status=403)
# request.auth is the token instance
device = request.auth.context_obj.device_id
return Response({'message': f'Hello from {device}!'})
Token Refresh
Implement a refresh endpoint to issue new access tokens without re-authentication:
from rest_framework.views import APIView
from rest_framework.response import Response
from drf_authentify.services import TokenService
class TokenRefreshView(APIView):
permission_classes = [] # No auth required
def post(self, request):
refresh_token = request.data.get('refresh_token')
if not refresh_token:
return Response({'error': 'refresh_token required'}, status=400)
new_token_set = TokenService.refresh_token(refresh_token)
if new_token_set:
return Response({
'access_token': new_token_set.access_token,
'refresh_token': new_token_set.refresh_token,
})
return Response({'error': 'Invalid refresh token'}, status=401)
Security: Old tokens are automatically revoked when refreshed.
Auto-Refresh
Enable automatic token renewal for active users:
# settings.py
from datetime import timedelta
DRF_AUTHENTIFY = {
'AUTO_REFRESH': True,
'AUTO_REFRESH_INTERVAL': timedelta(hours=1), # Minimum time between refreshes
'AUTO_REFRESH_MAX_TTL': timedelta(days=7), # Force re-login after 7 days
'TOKEN_TTL': timedelta(hours=12),
'REFRESH_TOKEN_TTL': timedelta(days=7),
}
With this enabled, tokens automatically renew during API requests, keeping active users logged in.
Configuration
Configure behavior by adding DRF_AUTHENTIFY to your settings.py:
from datetime import timedelta
DRF_AUTHENTIFY = {
# Token Lifespans
'TOKEN_TTL': timedelta(hours=24), # Access token duration
'REFRESH_TOKEN_TTL': timedelta(days=7), # Refresh token duration
# Auto-Refresh Settings
'AUTO_REFRESH': False, # Enable automatic renewal
'AUTO_REFRESH_INTERVAL': timedelta(hours=1), # Min time between refreshes
'AUTO_REFRESH_MAX_TTL': timedelta(days=7), # Max token age before forced re-login
# Authentication Behavior
'ENFORCE_SINGLE_LOGIN': False, # Revoke old tokens on new login
'ENABLE_AUTH_RESTRICTION': True, # Prevent cookie tokens in headers (and vice versa)
# Security
'SECURE_HASH_ALGORITHM': 'sha256', # Token hashing algorithm
'AUTH_HEADER_PREFIXES': ['Bearer', 'Token'], # Allowed header prefixes
'AUTH_COOKIE_NAMES': ['token'], # Cookie names to check
# Audit & Cleanup
'KEEP_EXPIRED_TOKENS': False, # Retain expired tokens for audit logs
# Advanced
'STRICT_CONTEXT_ACCESS': False, # Raise errors for undefined context keys
'TOKEN_MODEL': 'drf_authentify.AuthToken', # Custom token model path
'POST_AUTH_HANDLER': None, # Custom post-authentication function
'POST_AUTO_REFRESH_HANDLER': None, # Custom post-refresh function
}
Key Settings Explained
| Setting | Description |
|---|---|
TOKEN_TTL |
How long access tokens remain valid. Set to None for no expiration. |
REFRESH_TOKEN_TTL |
How long refresh tokens remain valid. Must be greater than TOKEN_TTL. Set to None to disable refresh tokens. |
AUTO_REFRESH |
When True, tokens automatically renew during API requests. Requires AUTO_REFRESH_INTERVAL and AUTO_REFRESH_MAX_TTL. |
AUTO_REFRESH_MAX_TTL |
Maximum token age before requiring full re-authentication, even with auto-refresh enabled. |
ENFORCE_SINGLE_LOGIN |
When True, creating a new token revokes all existing user tokens. |
ENABLE_AUTH_RESTRICTION |
When True, tokens created for cookies can't be used in headers and vice versa. |
KEEP_EXPIRED_TOKENS |
When True, expired tokens remain in the database for audit purposes (useful with ENFORCE_SINGLE_LOGIN). |
Common Tasks
Creating Tokens
For header-based authentication (mobile/API clients):
from drf_authentify.services import TokenService
token_set = TokenService.generate_header_token(
user=user,
context={"device": "mobile"},
access_expires_in=3600, # Optional: override TOKEN_TTL (in seconds)
refresh_expires_in=7200 # Optional: override REFRESH_TOKEN_TTL (in seconds)
)
For cookie-based authentication (web browsers):
token_set = TokenService.generate_cookie_token(
user=user,
context={"browser": "Chrome"},
access_expires_in=3600, # Optional: override TOKEN_TTL (in seconds)
refresh_expires_in=7200 # Optional: override REFRESH_TOKEN_TTL (in seconds)
)
# Set as httpOnly cookie in response
response.set_cookie(
'token',
token_set.access_token,
httponly=True,
secure=True,
samesite='Strict'
)
Accessing Token Information
In your views, request.auth provides the token instance:
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_view(request):
# Access context data
device = request.auth.context_obj.device
# Check expiration
if request.auth.is_expired:
return Response({'error': 'Token expired'}, status=401)
# Access token metadata
created = request.auth.created_at
expires = request.auth.expires_at
return Response({
'user': request.user.username,
'device': device,
'token_created': created
})
Revoking Tokens
from drf_authentify.services import TokenService
# Revoke a specific token
TokenService.revoke_token(request.auth)
# Revoke all tokens for a user (force logout everywhere)
TokenService.revoke_all_user_tokens(user)
# Revoke all expired tokens for a user
TokenService.revoke_all_expired_user_tokens(user)
# Clean up all expired tokens (run as scheduled task)
TokenService.revoke_expired_tokens()
Verifying Tokens Manually
from drf_authentify.services import TokenService
token_instance = TokenService.verify_token(
token_str="abc123...",
auth_type="header" # or "cookie"
)
if token_instance:
user = token_instance.user
# Token is valid
else:
# Invalid or expired token
pass
Advanced Usage
Custom Token Models
Extend the base token model with additional fields:
# myapp/models.py
from drf_authentify.models import AbstractAuthToken
class CustomAuthToken(AbstractAuthToken):
last_used_ip = models.GenericIPAddressField(null=True)
two_factor_verified = models.BooleanField(default=False)
class Meta:
db_table = 'custom_auth_tokens'
Then configure it:
# settings.py
DRF_AUTHENTIFY = {
'TOKEN_MODEL': 'myapp.CustomAuthToken',
}
Post-Authentication Hooks
Execute custom logic after authentication or token refresh:
# myapp/handlers.py
def my_post_auth_handler(user, token, token_str):
"""Called after successful authentication"""
# Update last login IP
token.last_used_ip = token.context.get('ip_address')
token.save()
# Must return (user, token) tuple
return user, token
def my_post_refresh_handler(user, token, token_str):
"""Called after successful token refresh"""
# Log refresh event
logger.info(f"Token refreshed for {user.username}")
return user, token
Configure in settings:
# settings.py
DRF_AUTHENTIFY = {
'POST_AUTH_HANDLER': 'myapp.handlers.my_post_auth_handler',
'POST_AUTO_REFRESH_HANDLER': 'myapp.handlers.my_post_refresh_handler',
}
Both handlers receive:
user- The authenticated user instancetoken- The token instance (AuthToken or your custom model)token_str- The raw token string
Both must return a tuple: (user, token)
Context-Based Authorization
Implement custom permissions based on token context:
from rest_framework.permissions import BasePermission
class RequireMobileDevice(BasePermission):
def has_permission(self, request, view):
if not request.auth:
return False
return request.auth.context_obj.device == "mobile"
# Use in views
@api_view(['GET'])
@permission_classes([IsAuthenticated, RequireMobileDevice])
def mobile_only_feature(request):
return Response({'message': 'Mobile exclusive content'})
Security Best Practices
1. Always Use HTTPS in Production
# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
2. Store Tokens Securely on Clients
Mobile apps: Use secure storage (Keychain, Keystore) Web apps: Use httpOnly cookies, never localStorage
// ❌ DON'T: Store in localStorage
localStorage.setItem('token', token);
// ✅ DO: Let server set httpOnly cookie
// Or use secure storage in mobile apps
3. Implement Rate Limiting
Protect authentication endpoints:
# Using django-ratelimit
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', method='POST')
def login_view(request):
# Your login logic
pass
4. Monitor Suspicious Activity
Use context data to detect anomalies:
def check_location_change(request):
"""Alert if token used from different location"""
stored_ip = request.auth.context_obj.ip_address
current_ip = request.META.get('REMOTE_ADDR')
if stored_ip != current_ip:
# Log suspicious activity
logger.warning(f"IP mismatch for {request.user}: {stored_ip} -> {current_ip}")
5. Set Appropriate Token Lifespans
Balance security and user experience:
DRF_AUTHENTIFY = {
# Short-lived access tokens
'TOKEN_TTL': timedelta(hours=1),
# Longer refresh tokens
'REFRESH_TOKEN_TTL': timedelta(days=7),
# Force full re-auth weekly
'AUTO_REFRESH_MAX_TTL': timedelta(days=7),
}
Troubleshooting
Tokens Not Working After Migration
Run migrations and restart your server:
python manage.py migrate drf_authentify
python manage.py runserver
"Invalid Token" Errors
Check that:
- The token exists and hasn't expired
- The correct authentication class is configured
- The token hash algorithm matches your settings
- The token is sent with the correct prefix (
BearerorToken)
Auto-Refresh Not Triggering
Ensure all three settings are configured:
DRF_AUTHENTIFY = {
'AUTO_REFRESH': True,
'AUTO_REFRESH_INTERVAL': timedelta(hours=1),
'AUTO_REFRESH_MAX_TTL': timedelta(days=7),
}
Context Data Not Available
Make sure you're accessing request.auth.context_obj, not request.auth.context:
# ✅ Correct
device = request.auth.context_obj.device
# ❌ Wrong
device = request.auth.context.device
Example: Complete Login/Logout Flow
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from django.contrib.auth import authenticate
from drf_authentify.services import TokenService
class LoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if not user:
return Response({'error': 'Invalid credentials'}, status=401)
# Generate token with context
token_set = TokenService.generate_header_token(
user=user,
context={
'device': request.data.get('device', 'unknown'),
'ip_address': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT', '')
}
)
return Response({
'access_token': token_set.access_token,
'refresh_token': token_set.refresh_token,
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
})
class LogoutView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# Revoke current token
TokenService.revoke_token(request.auth)
return Response({'message': 'Logged out successfully'})
class LogoutAllDevicesView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# Revoke all user tokens
TokenService.revoke_all_user_tokens(request.user)
return Response({'message': 'Logged out from all devices'})
Contributing
We welcome contributions! To get started:
- Fork the repository on GitHub
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes with tests
- Run the test suite
- Submit a pull request
Please ensure your code follows PEP 8 and includes appropriate tests.
License
Licensed under the BSD-3-Clause License. See LICENSE for details.
Resources
- GitHub: github.com/idenyigabriel/drf-authentify
- PyPI: pypi.org/project/drf-authentify
- Issues: GitHub Issues
Built with ❤️ for the Django community
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file drf_authentify-0.6.2.tar.gz.
File metadata
- Download URL: drf_authentify-0.6.2.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79eb4d6df0c187de729ea738d57df3ca84128dcb9476343e6963ca9cd70efe51
|
|
| MD5 |
61b34c330f458253bc0bde31365420d9
|
|
| BLAKE2b-256 |
17366cb5708f5afa553cd718f5b754f2d1fcbba187fe9910caa41312a464cae2
|