https://d226lax1qjow5r.cloudfront.net/blog/blogposts/2-factor-authentication-sms-voice-django-dr/python-django-2fa.png

Añade autenticación de dos factores a tu aplicación Django con Nexmo

Publicado el July 13, 2017

Tiempo de lectura: 12 minutos

He mostrado mi amor por la autenticación de dos factores antes en el blog de Vonage con una aplicación de demostración para mi "negocio "Kittens & Co. Curiosamente, no todo el mundo es igual de fan de los gatos, algunos preferimos a los perros y otros a otros animales, pero a todos nos encanta la autenticación de dos factores, ¿verdad?

Hagamos una pequeña encuesta

Para este tutorial, voy a mostrarte cómo agregar autenticación de dos factores a tu sitio Django utilizando la Verify API de Vonage. Para ello, he creado una pequeña aplicación llamada "Pollstr" - una simple aplicación web para hacer encuestas. Sé que va a ser un éxito de la noche a la mañana por la "e" que le falta al nombre. Quiero añadir dos factores de autenticación para asegurarme de que la gente es realmente quien dice ser, y para evitar el spam en mis encuestas.

Pollstr screenshotPollstr screenshot

Puede descargar el punto de partida de la aplicación desde Github y ejecutarla localmente.

# ensure you have Python and pip installed git clone https://github.com/nexmo-community/django-2fa-demo.git cd nexmo-django-2fa-demo pip install -r requirements.txt python manage.py migrate python manage.py loaddata fixtures/all.json python manage.py runserver

A continuación, visite 127.0.0.1:8000 en su navegador e intente votar en una encuesta. Puede iniciar sesión con estas credenciales:

  • nombre de usuario: test

  • contraseña: test1234

Por defecto, la aplicación implementa el registro y el inicio de sesión utilizando el marco de autenticación de Django, pero la mayor parte de este tutorial se aplica de manera similar a las aplicaciones que utilizan otros métodos de autenticación. Adicionalmente hemos añadido algo de bootstrap para mejorar la apariencia de nuestra aplicación.

Todo el código de este punto de partida se encuentra en la página antes de en Github. Todo el código que añadiremos a continuación se encuentra en la rama después de rama. Para tu comodidad puedes ver todos los cambios entre nuestro punto inicial y final en Github también.

Vonage Verify para 2FA

Vonage Verify es una forma segura y sin complicaciones de implementar la verificación telefónica en sólo 2 llamadas a la API. En la mayoría de los sistemas de autenticación de dos factores, deberás administrar tus propios tokens, la caducidad de los tokens, los reintentos y el envío de SMS. Vonage Verify administra todo esto por ti.

Para agregar Vonage Verify a nuestra aplicación, realizaremos los siguientes cambios:

  • Añadir un phone_number a nuestro usuario

  • Añadir un TwoFactorMixin a nuestras vistas para garantizar que el usuario está conectado y verificado

  • Grabar un nuevo número de teléfono para nuevos usuarios

  • Enviar al usuario un código de verificación

  • Verify the code sent to their number

Añadir un número de teléfono

El modelo de usuario por defecto en Django no tiene un número de teléfono, así que vamos a tener que añadir uno nosotros mismos. Hay varias formas de hacerlo, pero en este caso vamos a mantener todo nuestro código en una nueva aplicación two_factor app.

python manage.py startapp two_factor

Esto generará muchos archivos nuevos en la carpeta /two_factor carpeta. Abramos la carpeta /two_factor/models.py y añadamos un nuevo modelo que tenga una relación Uno a Uno con nuestro usuario.

# two_factor/models.py
...
from django.contrib.auth.models import User

class TwoFactor(models.Model):
    number = models.CharField(max_length=16)
    user = models.OneToOneField(User)

A continuación vamos a querer generar las migraciones para este modelo, pero para ello primero tenemos que asegurarnos de añadir two_factor.apps.TwoFactorConfig a nuestro fichero INSTALLED_APPS.

# pollstr/settings.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'two_factor.apps.TwoFactorConfig',
    'django.contrib.admin',
    ...
]

Con esto en su lugar podemos generar nuestras migraciones y migrar nuestra base de datos:

python manage.py makemigrations two_factor Migrations for 'two_factor': 0001_initial.py: - Create model TwoFactor python manage.py migrate Operations to perform: Apply all migrations: sessions, admin, two_factor, polls, auth, contenttypes Running migrations: Rendering model states... DONE ...

Añadir un TwoFactorMixin

Nuestra aplicación Django utiliza vistas basadas en clases que nos permiten utilizar "mixins" personalizados para añadir nuestro propio comportamiento a cada vista. Actualmente usamos el mixin LoginRequiredMixin para asegurarnos de que hemos iniciado sesión antes de poder votar en las encuestas.

# polls/views.yml
class OptionsView(LoginRequiredMixin, DetailView):
    ...

Vamos a implementar un nuevo TwoFactorMixin para añadir una capa TwoFactor a esta comprobación. Vamos a empezar por cambiar nuestras vistas para utilizar este nuevo mixin, aunque no lo hemos escrito todavía.

# polls/views.py
from two_factor.mixins import TwoFactorMixin

class OptionsView(TwoFactorMixin, DetailView):
    ...

class ResultsView(TwoFactorMixin, DetailView):
    ...

class VoteView(TwoFactorMixin, View):
    ...

Ahora vamos a añadir el mixin a nuestra two_factor aplicación:

# two_factor/mixins.py
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.urlresolvers import reverse

class TwoFactorMixin(UserPassesTestMixin):
    def test_func(self):
        user = self.request.user
        return (user.is_authenticated and "verified" in self.request.session)

    def get_login_url(self):
        if (self.request.user.is_authenticated()):
            return reverse('two_factor:new')
        else:
            return reverse('login')

Lo que hemos hecho aquí es crear un nuevo mixin que utiliza el método UserPassesTestMixin. Este mixin llama automáticamente a la función test_func donde comprobamos que el usuario está conectado y que la sesión ha sido verificada. Esto último lo hacemos simplemente comprobando si la clave verified en la sesión. Usando la sesión de esta manera, alguien puede estar conectado en múltiples máquinas mientras se requiere verificación para cada una de ellas.

La función get_login proporciona a la función UserPassesTestMixin con una ruta para redirigir al usuario si la prueba falla. En este caso tenemos 2 escenarios, uno en el que el usuario no ha iniciado sesión y otro en el que ha iniciado sesión pero no se ha verificado.

Si ejecutaras tu servidor en este punto fallaría porque, bueno, aún no hemos implementado ninguna de las rutas o vistas a las que redirigir al usuario. Vamos a hacer esto a continuación.

Seleccionar un número de teléfono

Screen Capture of Number Verification FormScreen Capture of Number Verification Form

Cuando el usuario necesita ser verificado se le redirige a two_factor:new donde le pediremos que establezca o confirme el número de teléfono al que le enviaremos un código.

# two_factor/urls.py
from django.conf.urls import url

from . import views

app_name = 'two_factor'
urlpatterns = [
    url(r'^$', views.NewView.as_view(), name='new'),
    url(r'^create/$', views.CreateView.as_view(), name='create'),
    url(r'^verify/$', views.VerifyView.as_view(), name='verify'),
]

También hemos añadido las URL para nuestros próximos pasos. A continuación, debemos asegurarnos de importar estas URL a nuestra aplicación principal.

# pollstr/urls.py
urlpatterns = [
    ...
    url(r'^polls/', include('polls.urls')),
    url(r'^2fa/', include('two_factor.urls')),
]

Cuando la aplicación redirija a /2fa/ intentará mostrar la vista NewView vista. Esta vista hará que el modelo TwoFactor disponible para la plantilla, pero tenemos que capturar la excepción obvia cuando el usuario no tiene un objeto TwoFactor e inicializar uno en su lugar.

# two_factor/views.py
from django.views.generic import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin

from .models import TwoFactor

class NewView(LoginRequiredMixin, DetailView):
    template_name = 'two_factor/new.html'

    def get_object(self):
        try:
            return self.request.user.twofactor
        except TwoFactor.DoesNotExist:
            return TwoFactor.objects.create(user=self.request.user)

Intentamos devolver el registro user.twofactor pero si no existe, inicializamos uno y lo devolvemos.

La vista muestra la plantilla two_factor/new.html que permitirá al usuario rellenar su número de teléfono, o muestra el número ya proporcionado en un campo desactivado. Ignoraremos el número en el campo deshabilitado más adelante si ya estaba configurado, pero es un buen recordatorio para el usuario de a qué número se enviará el código.

<!-- two_factor/templates/two_factor/new.html -->
{% extends 'polls/base.html' %}

{% block content %}

<form class='form-inline' action="{% url 'two_factor:create' %}" method="post">

  {% csrf_token %}
  <input type="hidden" name="next" value="{{ request.GET.next }}">

  <p>
    To continue we need to verify your phone number.
  </p>

  <div class="form-group">
    <input type="text" name="number" value="{{ object.number }}"
           {% if object.number %}disabled{% endif %} class='form-control'>
  </div>
  <div class="form-group">
    <input type="submit" name="name" value="Verify" class="btn btn-primary">
  </div>
</form>

{% endblock %}

Ignorando la sobrecarga de Bootstrap nuestro formulario es un formulario básico con unos pocos campos:

  • En number para enviar un código a

  • La página next a la que redirigir después de que hayamos terminado con la verificación, esta es una característica incorporada en Django así que vamos a jugar bien con esto.

Cuando el formulario se envíe a /2fa/create tendremos que enviar el código al usuario con Vonage.

Cómo usar Vonage Verify

Vonage Verify es muy fácil de usar y esencialmente se reduce a 2 llamadas a la API. La primera envía el código de verificación al número de teléfono del usuario. En nuestro caso, esto sucederá CreateView cuando se envía el formulario.

Para enviar el código necesitaremos la vonage librería Python. Ya hemos añadido esto a su requirements.txt junto con la librería django-dotenv que nos permitirá cargar nuestras credenciales desde un archivo .env archivo. Si prefieres otra forma de gestionar las dependencias de tu aplicación puedes instalarlas directamente con pip.

pip install nexmo pip install django-dotenv

La biblioteca vonage se puede instanciar con una clave de API y un secreto, o configurando algunas variables de entorno. Puedes obtener tu clave y secreto de la API de Vonage desde el dashboard.

# .env NEXMO_API_KEY=123 NEXMO_API_SECRET=234

Con estas variables de entorno configuradas ya no necesitamos inicializar nuestro cliente de Vonage y podemos utilizarlo directamente de la siguiente manera.

# two_factor/views.py
from django.views.generic import DetailView, View
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.contrib.auth import logout

import nexmo

from .models import TwoFactor

class CreateView(LoginRequiredMixin, View):
    def post(self, request):
        number = self.find_or_set_number(request)
        response = self.send_verification_request(request, number)

        if (response['status'] == '0'):
            request.session['verification_id'] = response['request_id']
            return HttpResponseRedirect(reverse('two_factor:verify')+"?next="+request.POST['next'])
        else:
            logout(request)
            messages.add_message(request, messages.INFO, 'Could not verify your number. Please contact support.')
            return HttpResponseRedirect('/')


    def find_or_set_number(self, request):
        two_factor = request.user.twofactor

        if (not two_factor.number):
            two_factor.number = request.POST['number']
            two_factor.save()

        return two_factor.number

    def send_verification_request(self, request, number):
        client = nexmo.Client()
        return client.start_verification(number=number, brand='Pollstr')

El código hace varias cosas. En primer lugar, utiliza find_or_set_number para comprobar si el usuario ya tiene un número de teléfono, y sólo si no está establecido se guardará el número que presentó.

A continuación, utiliza nexmo.Client().start_verification para iniciar el proceso de verificación. Aquí pasamos 2 parámetros: el number del usuario y un brand que aparecerá en el mensaje de texto que enviemos.

A continuación comprobamos si el status de nuestra llamada a la API es 0 y si lo es almacenamos el request_id de este intento de verificación en la sesión. Hacemos esto ya que necesitaremos este mismo id más tarde para confirmar el código que recibió el usuario.

Finalmente redirigimos al usuario a nuestra VerifyView que es una vista simple que muestra un formulario para introducir el código de verificación.

# two_factor/views.py
from django.views.generic import DetailView, View, TemplateView

class VerifyView(LoginRequiredMixin, TemplateView):
    template_name = 'two_factor/verify.html'

Y la plantilla correspondiente. Como puede ver, seguimos pasando el valor next para que podamos redirigir a la encuesta correcta al final.

<!-- two_factor/templates/two_factor/verify.html -->
{% extends 'polls/base.html' %}

{% block content %}

<form class="form-inline" action="{% url 'two_factor:confirm' %}" method="post">

  {% csrf_token %}
  <input type="hidden" name="next" value="{{request.GET.next}}">

  <p>
    We have sent a code to your number. Please type it in below.
  </p>

 <div class="form-group">
    <input type="text" name="code" class="form-control">

  </div>
  <div class="form-group">
    <input type="submit" name="name" value="Confirm" class="btn btn-primary">
  </div>
</form>

{% endblock %}

Verificación del código del usuario

Screengrab of 2 Factor Authentication FormScreengrab of 2 Factor Authentication Form

El último paso en este tutorial es confirmar el código que el usuario proporciona. Primero vamos a añadir la ruta para esta página.

# two_factor/urls.py
urlpatterns = [
    ...
    url(r'^confirm/$', views.ConfirmView.as_view(), name='confirm'),
]

En el paso anterior se presentó al usuario un formulario con un code campo Cuando lo envíe a la two_factor:verify URL tendremos que volver a llamar a la vonage con el código y la información request_id que almacenamos en la sesión anteriormente.

# two_factor/views.py
class ConfirmView(LoginRequiredMixin, View):
    def post(self, request):
        response = self.check_verification_request(request)

        if (response['status'] == '0'):
            request.session['verified'] = True
            return HttpResponseRedirect(request.POST['next'])
        else:
            messages.add_message(request, messages.INFO, 'Could not verify code. Please try again.')
            return HttpResponseRedirect(reverse('two_factor:verify')+"?next="+request.POST['next'])


    def check_verification_request(self, request):
        return nexmo.Client().check_verification(request.session['verification_id'], code=request.POST['code'])

Utilizamos la función nexmo.Client().check_verification para comprobar que el código request_id. Si se ha realizado correctamente el código de estado será 0 y marcamos la sesión como verificada. Cuando redirijamos al usuario a la página en la que empezó, la función TwoFactorMixin ya no redirigirá al usuario, sino que le permitirá ver la encuesta.

Using Vonage for 2 Authentication FactorUsing Vonage for 2 Authentication Factor

Próximos pasos

Hay muchas más opciones en la Verify API de Vonage que las que hemos cubierto aquí. El código que mostramos aquí es bastante simple y hay muchas maneras diferentes de implementar esta experiencia de usuario. El sistema Vonage Verify es extremadamente resistente, ya que vuelve a las llamadas telefónicas si es necesario, expira tokens sin que tengas que hacer nada, evita la reutilización de tokens y registra los tiempos de verificación.

La biblioteca Python de Vonage es muy agnóstica en cuanto a cómo se usa, lo que significa que podrías implementar cosas muy diferentes a las que hice aquí. Me encantaría saber qué añadirías a continuación. Envíame un tweet (soy @cbetta) con tus pensamientos e ideas.

Compartir:

https://a.storyblok.com/f/270183/169x169/d811e67494/cristiano-betta.png
Cristiano BettaAntiguos alumnos de Vonage