Autenticación#

Sistema de autenticación basado en correo electrónico con soporte multiempresa.

Descripción general#

Características clave:

  • Correo electrónico como nombre de usuario (sin campo de nombre de usuario separado)

  • Backend de autenticación personalizada

  • Acceso multiempresa sin necesidad de volver a iniciar sesión

  • integración django-allauth

  • Tokens JWT para API

Modelos:

  • Modelo de usuario personalizado (usuarios.Usuario)

  • Basado en sesiones para web

  • JWT para API

Modelo de usuario personalizado#

AUTH_USER_MODEL: usuarios.Usuario

class User(AbstractUser):
    username = None  # Removed
    email = models.EmailField(unique=True)

    companies = models.ManyToManyField(Company)
    active_company = models.ForeignKey(Company, null=True)
    nivel_acceso = models.ForeignKey(NivelAcceso)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

Ajustes:

# construbot/config/settings/base.py
AUTH_USER_MODEL = 'users.User'

AUTHENTICATION_BACKENDS = [
    'construbot.core.backends.ModelBackend',  # Custom
    'allauth.account.auth_backends.AuthenticationBackend',
]

Servidor de autenticación#

Archivo: construbot/core/backends.py

from django.contrib.auth.backends import ModelBackend as DjangoModelBackend

class ModelBackend(DjangoModelBackend):
    """Custom backend for email authentication"""

    def authenticate(self, request, username=None, password=None, **kwargs):
        # Email passed as 'username' parameter
        email = kwargs.get('email', username)

        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            return None

        if user.check_password(password):
            return user
        return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Autenticación web#

Iniciar sesión Ver#

from django.contrib.auth import authenticate, login

def login_view(request):
    if request.method == 'POST':
        email = request.POST.get('email')
        password = request.POST.get('password')

        user = authenticate(request, email=email, password=password)

        if user is not None:
            login(request, user)

            # Set active company
            if not user.active_company and user.companies.exists():
                user.active_company = user.companies.first()
                user.save()

            return redirect('dashboard')
        else:
            messages.error(request, 'Invalid credentials')

    return render(request, 'account/login.html')

Gestión de sesiones#

Sesiones almacenadas en:

  • Base de datos (predeterminada): django.contrib.sessions.backends.db

  • Caché (más rápido): django.contrib.sessions.backends.cache

  • Base de datos en caché (recomendado): django.contrib.sessions.backends.cached_db

Ajustes:

# Production recommended
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
SESSION_COOKIE_AGE = 1209600  # 2 weeks
SESSION_COOKIE_SECURE = True  # HTTPS only
SESSION_COOKIE_HTTPONLY = True  # No JavaScript access

Contexto de la empresa en la sesión#

# After login, active_company stored in User model
request.user.active_company  # Current company

# Switch company
def switch_company(request, company_id):
    company = get_object_or_404(Company, id=company_id)
    if company in request.user.companies.all():
        request.user.active_company = company
        request.user.save()
    return redirect('dashboard')

Integración Django Allauth#

Configuración:

INSTALLED_APPS = [
    'allauth',
    'allauth.account',
    'allauth.socialaccount',  # Optional
]

# Allauth settings
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None

URL:

urlpatterns = [
    path('accounts/', include('allauth.urls')),
]

Plantillas:

Anule todas las plantillas de autenticación en templates/account/:

  • iniciar sesión.html

  • registro.html

  • contraseña_reset.html

Autenticación API#

Configuración JWT#

Paquetes:

  • marcodjangorest

  • djangorestframework-simplejwt

Ajustes:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
}

URL:

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('api/v1/api-token-auth/', TokenObtainPairView.as_view()),
    path('api/v1/api-token-refresh/', TokenRefreshView.as_view()),
]

Obtener fichas#

Pedido:

curl -X POST http://localhost:8000/api/v1/api-token-auth/ \\
  -H "Content-Type: application/json" \\
  -d '{"email":"user@example.com","password":"password"}'

Respuesta:

{
  "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

Usando fichas#

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLC..." \\
  http://localhost:8000/api/v1/contracts/

Fichas refrescantes#

curl -X POST http://localhost:8000/api/v1/api-token-refresh/ \\
  -H "Content-Type: application/json" \\
  -d '{"refresh":"eyJ0eXAiOiJKV1QiLC..."}'

Gestión de contraseñas#

Hash de contraseña#

Argón2 (recomendado):

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]

Requiere:

pip install argon2-cffi

Restablecer contraseña#

URL:

/accounts/password/reset/

Plantilla de correo electrónico:

Cree plantillas/cuenta/correo electrónico/contraseña_reset_key_message.txt

Fluir:

  1. Restablecimiento de solicitudes de usuario → correo electrónico enviado

  2. Haga clic en el enlace → ingrese una nueva contraseña

  3. Contraseña actualizada → puede iniciar sesión

Cambiar la contraseña#

from django.contrib.auth.views import PasswordChangeView

path('accounts/password/change/', PasswordChangeView.as_view())

Permisos y autorización#

Iniciar sesión requerido#

from django.contrib.auth.decorators import login_required

@login_required
def dashboard(request):
    return render(request, 'dashboard.html')

Vistas basadas en clases:

from django.contrib.auth.mixins import LoginRequiredMixin

class DashboardView(LoginRequiredMixin, TemplateView):
    template_name = 'dashboard.html'

Verificación del nivel de permiso#

def director_required(function):
    def wrap(request, *args, **kwargs):
        if request.user.nivel_acceso.nivel >= 3:
            return function(request, *args, **kwargs)
        return HttpResponseForbidden()
    return wrap

@login_required
@director_required
def sensitive_view(request):
    ...

Consulte Niveles de permiso para obtener más detalles.

Mejores prácticas de seguridad#

1. Utilice HTTPS en producción:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

2. Requisitos de contraseña fuertes:

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 8}},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

3. Limitación de tarifa:

Utilice django-ratelimit o similar.

4. Autenticación de dos factores (opcional):

Utilice django-otp o django-allauth 2FA.

5. Seguridad de la sesión:

SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'

Solución de problemas#

«La consulta de coincidencia del usuario no existe»:

  • Compruebe que el correo electrónico sea correcto

  • Verificar que el usuario exista en la base de datos

  • Confirmar la configuración AUTH_USER_MODEL

«Credenciales no válidas»:

  • Verificar que la contraseña sea correcta

  • Verifique que el backend de autenticación esté configurado

  • Asegúrese de que el usuario esté activo (is_active=True)

API 401 no autorizado:

  • El token de cheque está incluido en el encabezado

  • Verificar que el token no haya caducado

  • Confirmar formato del token: Portador <token>

La sesión no persiste:

  • Verifique SESSION_COOKIE_SECURE con HTTP (debe ser Falso para local)

  • Verifique que el navegador acepte cookies

  • Verifique que el backend de la sesión esté configurado

Ver también#

  • Multi-inquilino - Multi-company architecture

  • Niveles de permiso - Permission system

  • ../api/authentication - API auth details

  • ../models/users - User model reference