https://github.com/idenyigabriel/drf-authentify
A simple authentication module for django rest framework
https://github.com/idenyigabriel/drf-authentify
authentication django django-rest-framework
Last synced: 3 months ago
JSON representation
A simple authentication module for django rest framework
- Host: GitHub
- URL: https://github.com/idenyigabriel/drf-authentify
- Owner: idenyigabriel
- License: bsd-3-clause
- Created: 2024-03-15T20:02:39.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2025-11-23T10:33:01.000Z (7 months ago)
- Last Synced: 2025-11-23T12:09:19.232Z (7 months ago)
- Topics: authentication, django, django-rest-framework
- Language: Python
- Homepage: https://pypi.org/project/drf-authentify/
- Size: 111 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# 🔒 DRF Authentify
[](https://github.com/idenyigabriel/drf-authentify/actions/workflows/test.yml)
[](https://opensource.org/licenses/BSD-3-Clause)
**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
```bash
pip install drf-authentify
```
**Requirements:** Python ≥ 3.9, Django ≥ 3.2, Django REST Framework ≥ 3.0
---
## Quick Start
### 1. Add to Your Project
```python
# settings.py
INSTALLED_APPS = [
# ... your apps
'drf_authentify',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'drf_authentify.auth.AuthorizationHeaderAuthentication',
'drf_authentify.auth.CookieAuthentication',
],
}
```
### 2. Run Migrations
```bash
python manage.py migrate
```
### 3. Create Your First Token
```python
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
```
---
## Core Concepts
### Multi-Device Authentication
Users can maintain multiple active sessions across different devices. Each token stores its own context:
```python
# 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:
```python
# settings.py
DRF_AUTHENTIFY = {
'ENFORCE_SINGLE_LOGIN': True,
}
```
### Session Context
Store custom metadata with each token for authorization decisions:
```python
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:
```python
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:
```python
# 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`:
```python
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):**
```python
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):**
```python
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:
```python
@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
```python
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
```python
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:
```python
# 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:
```python
# settings.py
DRF_AUTHENTIFY = {
'TOKEN_MODEL': 'myapp.CustomAuthToken',
}
```
### Post-Authentication Hooks
Execute custom logic after authentication or token refresh:
```python
# 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:
```python
# 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 instance
- `token` - 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:
```python
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
```python
# 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
```javascript
// ❌ 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:
```python
# 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:
```python
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:
```python
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:
```bash
python manage.py migrate drf_authentify
python manage.py runserver
```
### "Invalid Token" Errors
Check that:
1. The token exists and hasn't expired
2. The correct authentication class is configured
3. The token hash algorithm matches your settings
4. The token is sent with the correct prefix (`Bearer` or `Token`)
### Auto-Refresh Not Triggering
Ensure all three settings are configured:
```python
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`:
```python
# ✅ Correct
device = request.auth.context_obj.device
# ❌ Wrong
device = request.auth.context.device
```
---
## Example: Complete Login/Logout Flow
```python
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:
1. Fork the repository on GitHub
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Make your changes with tests
4. Run the test suite
5. 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](LICENSE) for details.
---
## Resources
- **GitHub:** [github.com/idenyigabriel/drf-authentify](https://github.com/idenyigabriel/drf-authentify)
- **PyPI:** [pypi.org/project/drf-authentify](https://pypi.org/project/drf-authentify/)
- **Issues:** [GitHub Issues](https://github.com/idenyigabriel/drf-authentify/issues)
---
**Built with ❤️ for the Django community**