{"id":35260768,"url":"https://github.com/idenyigabriel/drf-authentify","last_synced_at":"2026-04-10T05:10:09.539Z","repository":{"id":239800431,"uuid":"772744191","full_name":"idenyigabriel/drf-authentify","owner":"idenyigabriel","description":"A simple authentication module for django rest framework","archived":false,"fork":false,"pushed_at":"2025-11-23T10:33:01.000Z","size":114,"stargazers_count":2,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-23T12:09:19.232Z","etag":null,"topics":["authentication","django","django-rest-framework"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/drf-authentify/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/idenyigabriel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-03-15T20:02:39.000Z","updated_at":"2025-11-23T10:33:05.000Z","dependencies_parsed_at":"2024-05-15T13:15:17.361Z","dependency_job_id":"aeff30a5-6bbb-4709-8f3e-eedbe0a208c2","html_url":"https://github.com/idenyigabriel/drf-authentify","commit_stats":null,"previous_names":["idenyigabriel/drf-authentify"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/idenyigabriel/drf-authentify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/idenyigabriel%2Fdrf-authentify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/idenyigabriel%2Fdrf-authentify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/idenyigabriel%2Fdrf-authentify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/idenyigabriel%2Fdrf-authentify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/idenyigabriel","download_url":"https://codeload.github.com/idenyigabriel/drf-authentify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/idenyigabriel%2Fdrf-authentify/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28006388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","django","django-rest-framework"],"created_at":"2025-12-30T09:04:34.037Z","updated_at":"2025-12-30T09:05:59.868Z","avatar_url":"https://github.com/idenyigabriel.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🔒 DRF Authentify\n\n[![Build Status](https://github.com/idenyigabriel/drf-authentify/actions/workflows/test.yml/badge.svg)](https://github.com/idenyigabriel/drf-authentify/actions/workflows/test.yml)\n[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)\n\n**Modern token authentication for Django Rest Framework with multi-device support, auto-refresh, and session context.**\n\n---\n\n## Why Choose DRF Authentify?\n\nDRF Authentify reimagines token authentication for modern applications. Unlike DRF's default token system, it provides:\n\n- **Multi-device sessions** - Users stay logged in across mobile, web, and desktop simultaneously\n- **Session context** - Store device info, IP addresses, and custom metadata with each token\n- **Auto-refresh** - Tokens renew automatically during active use\n- **Flexible security** - Choose between single-login enforcement or multiple active sessions\n- **Production-ready** - Secure token hashing, expiration management, and audit trails\n\n---\n\n## Installation\n\n```bash\npip install drf-authentify\n```\n\n**Requirements:** Python ≥ 3.9, Django ≥ 3.2, Django REST Framework ≥ 3.0\n\n---\n\n## Quick Start\n\n### 1. Add to Your Project\n\n```python\n# settings.py\n\nINSTALLED_APPS = [\n    # ... your apps\n    'drf_authentify',\n]\n\nREST_FRAMEWORK = {\n    'DEFAULT_AUTHENTICATION_CLASSES': [\n        'drf_authentify.auth.AuthorizationHeaderAuthentication',\n        'drf_authentify.auth.CookieAuthentication',\n    ],\n}\n```\n\n### 2. Run Migrations\n\n```bash\npython manage.py migrate\n```\n\n### 3. Create Your First Token\n\n```python\nfrom drf_authentify.services import TokenService\n\n# In your login view\ntoken_set = TokenService.generate_header_token(\n    user=request.user,\n    context={\n        \"device\": \"mobile\",\n        \"ip_address\": request.META.get('REMOTE_ADDR')\n    }\n)\n\n# Return to client\nreturn Response({\n    'access_token': token_set.access_token,\n    'refresh_token': token_set.refresh_token,\n})\n```\n\nYour API is now protected! Clients authenticate by sending:\n\n```\nAuthorization: Bearer \u003caccess_token\u003e\n```\n\n---\n\n## Core Concepts\n\n### Multi-Device Authentication\n\nUsers can maintain multiple active sessions across different devices. Each token stores its own context:\n\n```python\n# Mobile login\nmobile_token = TokenService.generate_header_token(\n    user=user,\n    context={\"device\": \"iPhone\", \"app_version\": \"2.1\"}\n)\n\n# Web login (doesn't invalidate mobile token)\nweb_token = TokenService.generate_header_token(\n    user=user,\n    context={\"device\": \"Chrome\", \"browser_version\": \"120\"}\n)\n```\n\nTo enforce single-device login instead:\n\n```python\n# settings.py\nDRF_AUTHENTIFY = {\n    'ENFORCE_SINGLE_LOGIN': True,\n}\n```\n\n### Session Context\n\nStore custom metadata with each token for authorization decisions:\n\n```python\ntoken_set = TokenService.generate_header_token(\n    user=user,\n    context={\n        \"device_id\": \"abc-123\",\n        \"location\": \"US\",\n        \"beta_features\": True,\n        \"subscription_tier\": \"premium\"\n    }\n)\n\n# Access in your views\n@api_view(['GET'])\n@permission_classes([IsAuthenticated])\ndef premium_feature(request):\n    if not request.auth.context_obj.beta_features:\n        return Response({'error': 'Beta access required'}, status=403)\n    \n    # request.auth is the token instance\n    device = request.auth.context_obj.device_id\n    return Response({'message': f'Hello from {device}!'})\n```\n\n### Token Refresh\n\nImplement a refresh endpoint to issue new access tokens without re-authentication:\n\n```python\nfrom rest_framework.views import APIView\nfrom rest_framework.response import Response\nfrom drf_authentify.services import TokenService\n\nclass TokenRefreshView(APIView):\n    permission_classes = []  # No auth required\n    \n    def post(self, request):\n        refresh_token = request.data.get('refresh_token')\n        \n        if not refresh_token:\n            return Response({'error': 'refresh_token required'}, status=400)\n        \n        new_token_set = TokenService.refresh_token(refresh_token)\n        \n        if new_token_set:\n            return Response({\n                'access_token': new_token_set.access_token,\n                'refresh_token': new_token_set.refresh_token,\n            })\n        \n        return Response({'error': 'Invalid refresh token'}, status=401)\n```\n\n**Security:** Old tokens are automatically revoked when refreshed.\n\n### Auto-Refresh\n\nEnable automatic token renewal for active users:\n\n```python\n# settings.py\nfrom datetime import timedelta\n\nDRF_AUTHENTIFY = {\n    'AUTO_REFRESH': True,\n    'AUTO_REFRESH_INTERVAL': timedelta(hours=1),     # Minimum time between refreshes\n    'AUTO_REFRESH_MAX_TTL': timedelta(days=7),       # Force re-login after 7 days\n    'TOKEN_TTL': timedelta(hours=12),\n    'REFRESH_TOKEN_TTL': timedelta(days=7),\n}\n```\n\nWith this enabled, tokens automatically renew during API requests, keeping active users logged in.\n\n---\n\n## Configuration\n\nConfigure behavior by adding `DRF_AUTHENTIFY` to your `settings.py`:\n\n```python\nfrom datetime import timedelta\n\nDRF_AUTHENTIFY = {\n    # Token Lifespans\n    'TOKEN_TTL': timedelta(hours=24),              # Access token duration\n    'REFRESH_TOKEN_TTL': timedelta(days=7),        # Refresh token duration\n    \n    # Auto-Refresh Settings\n    'AUTO_REFRESH': False,                         # Enable automatic renewal\n    'AUTO_REFRESH_INTERVAL': timedelta(hours=1),   # Min time between refreshes\n    'AUTO_REFRESH_MAX_TTL': timedelta(days=7),     # Max token age before forced re-login\n    \n    # Authentication Behavior\n    'ENFORCE_SINGLE_LOGIN': False,                 # Revoke old tokens on new login\n    'ENABLE_AUTH_RESTRICTION': True,               # Prevent cookie tokens in headers (and vice versa)\n    \n    # Security\n    'SECURE_HASH_ALGORITHM': 'sha256',             # Token hashing algorithm\n    'AUTH_HEADER_PREFIXES': ['Bearer', 'Token'],   # Allowed header prefixes\n    'AUTH_COOKIE_NAMES': ['token'],                # Cookie names to check\n    \n    # Audit \u0026 Cleanup\n    'KEEP_EXPIRED_TOKENS': False,                  # Retain expired tokens for audit logs\n    \n    # Advanced\n    'STRICT_CONTEXT_ACCESS': False,                # Raise errors for undefined context keys\n    'TOKEN_MODEL': 'drf_authentify.AuthToken',     # Custom token model path\n    'POST_AUTH_HANDLER': None,                     # Custom post-authentication function\n    'POST_AUTO_REFRESH_HANDLER': None,             # Custom post-refresh function\n}\n```\n\n### Key Settings Explained\n\n| Setting | Description |\n|---------|-------------|\n| `TOKEN_TTL` | How long access tokens remain valid. Set to `None` for no expiration. |\n| `REFRESH_TOKEN_TTL` | How long refresh tokens remain valid. Must be greater than `TOKEN_TTL`. Set to `None` to disable refresh tokens. |\n| `AUTO_REFRESH` | When `True`, tokens automatically renew during API requests. Requires `AUTO_REFRESH_INTERVAL` and `AUTO_REFRESH_MAX_TTL`. |\n| `AUTO_REFRESH_MAX_TTL` | Maximum token age before requiring full re-authentication, even with auto-refresh enabled. |\n| `ENFORCE_SINGLE_LOGIN` | When `True`, creating a new token revokes all existing user tokens. |\n| `ENABLE_AUTH_RESTRICTION` | When `True`, tokens created for cookies can't be used in headers and vice versa. |\n| `KEEP_EXPIRED_TOKENS` | When `True`, expired tokens remain in the database for audit purposes (useful with `ENFORCE_SINGLE_LOGIN`). |\n\n---\n\n## Common Tasks\n\n### Creating Tokens\n\n**For header-based authentication (mobile/API clients):**\n\n```python\nfrom drf_authentify.services import TokenService\n\ntoken_set = TokenService.generate_header_token(\n    user=user,\n    context={\"device\": \"mobile\"},\n    access_expires_in=3600,   # Optional: override TOKEN_TTL (in seconds)\n    refresh_expires_in=7200   # Optional: override REFRESH_TOKEN_TTL (in seconds)\n)\n```\n\n**For cookie-based authentication (web browsers):**\n\n```python\ntoken_set = TokenService.generate_cookie_token(\n    user=user,\n    context={\"browser\": \"Chrome\"},\n    access_expires_in=3600,   # Optional: override TOKEN_TTL (in seconds)\n    refresh_expires_in=7200   # Optional: override REFRESH_TOKEN_TTL (in seconds)\n)\n\n# Set as httpOnly cookie in response\nresponse.set_cookie(\n    'token',\n    token_set.access_token,\n    httponly=True,\n    secure=True,\n    samesite='Strict'\n)\n```\n\n### Accessing Token Information\n\nIn your views, `request.auth` provides the token instance:\n\n```python\n@api_view(['GET'])\n@permission_classes([IsAuthenticated])\ndef profile_view(request):\n    # Access context data\n    device = request.auth.context_obj.device\n    \n    # Check expiration\n    if request.auth.is_expired:\n        return Response({'error': 'Token expired'}, status=401)\n    \n    # Access token metadata\n    created = request.auth.created_at\n    expires = request.auth.expires_at\n    \n    return Response({\n        'user': request.user.username,\n        'device': device,\n        'token_created': created\n    })\n```\n\n### Revoking Tokens\n\n```python\nfrom drf_authentify.services import TokenService\n\n# Revoke a specific token\nTokenService.revoke_token(request.auth)\n\n# Revoke all tokens for a user (force logout everywhere)\nTokenService.revoke_all_user_tokens(user)\n\n# Revoke all expired tokens for a user\nTokenService.revoke_all_expired_user_tokens(user)\n\n# Clean up all expired tokens (run as scheduled task)\nTokenService.revoke_expired_tokens()\n```\n\n### Verifying Tokens Manually\n\n```python\nfrom drf_authentify.services import TokenService\n\ntoken_instance = TokenService.verify_token(\n    token_str=\"abc123...\",\n    auth_type=\"header\"  # or \"cookie\"\n)\n\nif token_instance:\n    user = token_instance.user\n    # Token is valid\nelse:\n    # Invalid or expired token\n    pass\n```\n\n---\n\n## Advanced Usage\n\n### Custom Token Models\n\nExtend the base token model with additional fields:\n\n```python\n# myapp/models.py\nfrom drf_authentify.models import AbstractAuthToken\n\nclass CustomAuthToken(AbstractAuthToken):\n    last_used_ip = models.GenericIPAddressField(null=True)\n    two_factor_verified = models.BooleanField(default=False)\n    \n    class Meta:\n        db_table = 'custom_auth_tokens'\n```\n\nThen configure it:\n\n```python\n# settings.py\nDRF_AUTHENTIFY = {\n    'TOKEN_MODEL': 'myapp.CustomAuthToken',\n}\n```\n\n### Post-Authentication Hooks\n\nExecute custom logic after authentication or token refresh:\n\n```python\n# myapp/handlers.py\ndef my_post_auth_handler(user, token, token_str):\n    \"\"\"Called after successful authentication\"\"\"\n    # Update last login IP\n    token.last_used_ip = token.context.get('ip_address')\n    token.save()\n    \n    # Must return (user, token) tuple\n    return user, token\n\ndef my_post_refresh_handler(user, token, token_str):\n    \"\"\"Called after successful token refresh\"\"\"\n    # Log refresh event\n    logger.info(f\"Token refreshed for {user.username}\")\n    return user, token\n```\n\nConfigure in settings:\n\n```python\n# settings.py\nDRF_AUTHENTIFY = {\n    'POST_AUTH_HANDLER': 'myapp.handlers.my_post_auth_handler',\n    'POST_AUTO_REFRESH_HANDLER': 'myapp.handlers.my_post_refresh_handler',\n}\n```\n\nBoth handlers receive:\n- `user` - The authenticated user instance\n- `token` - The token instance (AuthToken or your custom model)\n- `token_str` - The raw token string\n\nBoth must return a tuple: `(user, token)`\n\n### Context-Based Authorization\n\nImplement custom permissions based on token context:\n\n```python\nfrom rest_framework.permissions import BasePermission\n\nclass RequireMobileDevice(BasePermission):\n    def has_permission(self, request, view):\n        if not request.auth:\n            return False\n        return request.auth.context_obj.device == \"mobile\"\n\n# Use in views\n@api_view(['GET'])\n@permission_classes([IsAuthenticated, RequireMobileDevice])\ndef mobile_only_feature(request):\n    return Response({'message': 'Mobile exclusive content'})\n```\n\n---\n\n## Security Best Practices\n\n### 1. Always Use HTTPS in Production\n\n```python\n# settings.py\nSECURE_SSL_REDIRECT = True\nSESSION_COOKIE_SECURE = True\nCSRF_COOKIE_SECURE = True\n```\n\n### 2. Store Tokens Securely on Clients\n\n**Mobile apps:** Use secure storage (Keychain, Keystore)\n**Web apps:** Use httpOnly cookies, never localStorage\n\n```javascript\n// ❌ DON'T: Store in localStorage\nlocalStorage.setItem('token', token);\n\n// ✅ DO: Let server set httpOnly cookie\n// Or use secure storage in mobile apps\n```\n\n### 3. Implement Rate Limiting\n\nProtect authentication endpoints:\n\n```python\n# Using django-ratelimit\nfrom django_ratelimit.decorators import ratelimit\n\n@ratelimit(key='ip', rate='5/m', method='POST')\ndef login_view(request):\n    # Your login logic\n    pass\n```\n\n### 4. Monitor Suspicious Activity\n\nUse context data to detect anomalies:\n\n```python\ndef check_location_change(request):\n    \"\"\"Alert if token used from different location\"\"\"\n    stored_ip = request.auth.context_obj.ip_address\n    current_ip = request.META.get('REMOTE_ADDR')\n    \n    if stored_ip != current_ip:\n        # Log suspicious activity\n        logger.warning(f\"IP mismatch for {request.user}: {stored_ip} -\u003e {current_ip}\")\n```\n\n### 5. Set Appropriate Token Lifespans\n\nBalance security and user experience:\n\n```python\nDRF_AUTHENTIFY = {\n    # Short-lived access tokens\n    'TOKEN_TTL': timedelta(hours=1),\n    \n    # Longer refresh tokens\n    'REFRESH_TOKEN_TTL': timedelta(days=7),\n    \n    # Force full re-auth weekly\n    'AUTO_REFRESH_MAX_TTL': timedelta(days=7),\n}\n```\n\n---\n\n## Troubleshooting\n\n### Tokens Not Working After Migration\n\nRun migrations and restart your server:\n\n```bash\npython manage.py migrate drf_authentify\npython manage.py runserver\n```\n\n### \"Invalid Token\" Errors\n\nCheck that:\n1. The token exists and hasn't expired\n2. The correct authentication class is configured\n3. The token hash algorithm matches your settings\n4. The token is sent with the correct prefix (`Bearer` or `Token`)\n\n### Auto-Refresh Not Triggering\n\nEnsure all three settings are configured:\n\n```python\nDRF_AUTHENTIFY = {\n    'AUTO_REFRESH': True,\n    'AUTO_REFRESH_INTERVAL': timedelta(hours=1),\n    'AUTO_REFRESH_MAX_TTL': timedelta(days=7),\n}\n```\n\n### Context Data Not Available\n\nMake sure you're accessing `request.auth.context_obj`, not `request.auth.context`:\n\n```python\n# ✅ Correct\ndevice = request.auth.context_obj.device\n\n# ❌ Wrong\ndevice = request.auth.context.device\n```\n\n---\n\n## Example: Complete Login/Logout Flow\n\n```python\nfrom rest_framework import status\nfrom rest_framework.views import APIView\nfrom rest_framework.response import Response\nfrom rest_framework.permissions import IsAuthenticated, AllowAny\nfrom django.contrib.auth import authenticate\nfrom drf_authentify.services import TokenService\n\nclass LoginView(APIView):\n    permission_classes = [AllowAny]\n    \n    def post(self, request):\n        username = request.data.get('username')\n        password = request.data.get('password')\n        \n        user = authenticate(username=username, password=password)\n        if not user:\n            return Response({'error': 'Invalid credentials'}, status=401)\n        \n        # Generate token with context\n        token_set = TokenService.generate_header_token(\n            user=user,\n            context={\n                'device': request.data.get('device', 'unknown'),\n                'ip_address': request.META.get('REMOTE_ADDR'),\n                'user_agent': request.META.get('HTTP_USER_AGENT', '')\n            }\n        )\n        \n        return Response({\n            'access_token': token_set.access_token,\n            'refresh_token': token_set.refresh_token,\n            'user': {\n                'id': user.id,\n                'username': user.username,\n                'email': user.email\n            }\n        })\n\nclass LogoutView(APIView):\n    permission_classes = [IsAuthenticated]\n    \n    def post(self, request):\n        # Revoke current token\n        TokenService.revoke_token(request.auth)\n        return Response({'message': 'Logged out successfully'})\n\nclass LogoutAllDevicesView(APIView):\n    permission_classes = [IsAuthenticated]\n    \n    def post(self, request):\n        # Revoke all user tokens\n        TokenService.revoke_all_user_tokens(request.user)\n        return Response({'message': 'Logged out from all devices'})\n```\n\n---\n\n## Contributing\n\nWe welcome contributions! To get started:\n\n1. Fork the repository on GitHub\n2. Create a feature branch (`git checkout -b feature/my-feature`)\n3. Make your changes with tests\n4. Run the test suite\n5. Submit a pull request\n\nPlease ensure your code follows PEP 8 and includes appropriate tests.\n\n---\n\n## License\n\nLicensed under the **BSD-3-Clause License**. See [LICENSE](LICENSE) for details.\n\n---\n\n## Resources\n\n- **GitHub:** [github.com/idenyigabriel/drf-authentify](https://github.com/idenyigabriel/drf-authentify)\n- **PyPI:** [pypi.org/project/drf-authentify](https://pypi.org/project/drf-authentify/)\n- **Issues:** [GitHub Issues](https://github.com/idenyigabriel/drf-authentify/issues)\n\n---\n\n**Built with ❤️ for the Django community**","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fidenyigabriel%2Fdrf-authentify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fidenyigabriel%2Fdrf-authentify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fidenyigabriel%2Fdrf-authentify/lists"}