Metadata-Version: 2.4
Name: drf-authentify
Version: 0.6.2
Summary: A simple authentication module for django rest framework
Author: Gabriel Idenyi
License: BSD 3-Clause License
        
        Copyright (c) 2024, Idenyi Gabriel
        
        Redistribution and use in source and binary forms, with or without
        modification, are permitted provided that the following conditions are met:
        
        1. Redistributions of source code must retain the above copyright notice, this
           list of conditions and the following disclaimer.
        
        2. Redistributions in binary form must reproduce the above copyright notice,
           this list of conditions and the following disclaimer in the documentation
           and/or other materials provided with the distribution.
        
        3. Neither the name of the copyright holder nor the names of its
           contributors may be used to endorse or promote products derived from
           this software without specific prior written permission.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
        AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
        IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
        FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
        DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
        SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
        CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
        OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
        OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
        
Project-URL: Homepage, https://github.com/idenyigabriel/drf-authentify
Project-URL: Documentation, https://github.com/idenyigabriel/drf-authentify/blob/main/README.md
Project-URL: Repository, https://github.com/idenyigabriel/drf-authentify
Project-URL: Issues, https://github.com/idenyigabriel/drf-authentify/issues
Project-URL: Changelog, https://github.com/idenyigabriel/drf-authentify/blob/main/CHANGELOG.md
Keywords: django,djangorestframework,drf,authentication
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Requires-Dist: djangorestframework>=3.1
Requires-Dist: typing_extensions; python_version < "3.11"
Dynamic: license-file

# 🔒 DRF Authentify

[![Build Status](https://github.com/idenyigabriel/drf-authentify/actions/workflows/test.yml/badge.svg)](https://github.com/idenyigabriel/drf-authentify/actions/workflows/test.yml)
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](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 <access_token>
```

---

## 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**
