Cómo integrar OAuth 2 en su back-end Django/DRF sin volverse loco
Publicado: 2022-03-11Todos hemos estado allí. Está trabajando en el back-end de la API y está contento con cómo va. Recientemente completó el producto mínimo viable (MVP), todas las pruebas están pasando y está ansioso por implementar algunas funciones nuevas.
Luego, el jefe te envía un correo electrónico: “Por cierto, debemos permitir que las personas inicien sesión a través de Facebook y Google; no deberían tener que crear una cuenta solo para un pequeño sitio como el nuestro”.
Genial. La fluencia del alcance ataca de nuevo.
La buena noticia es que OAuth 2 se ha convertido en el estándar de la industria para la autenticación social y de terceros (utilizado por servicios como Facebook, Google, etc.) para que pueda concentrarse en comprender e implementar ese estándar para admitir una amplia gama de redes sociales. proveedores de autenticación.
Es probable que no esté familiarizado con OAuth 2; No lo estaba, cuando esto me pasó a mí.
Como desarrollador de Python, su instinto puede llevarlo a pip, la herramienta recomendada por Python Package Index (PyPA) para instalar paquetes de Python. La mala noticia es que pip conoce 278 paquetes que se ocupan de OAuth, 53 de los cuales mencionan específicamente a Django. Es el trabajo de una semana solo para investigar las opciones, no importa comenzar a escribir código.
En este tutorial, aprenderá cómo integrar OAuth 2 en su Django o Django Rest Framework usando Python Social Auth. Aunque este artículo se centra en Django REST Framework, puede aplicar la información proporcionada aquí para implementar lo mismo en una variedad de otros marcos de back-end comunes.
Una descripción general rápida del flujo de OAuth 2
OAuth 2 fue diseñado desde el principio como un protocolo de autenticación web. Esto no es lo mismo que si hubiera sido diseñado como un protocolo de autenticación de red; asume que usted tiene a su disposición herramientas como la representación de HTML y los redireccionamientos del navegador.
Obviamente, esto es un obstáculo para una API basada en JSON, pero puede solucionarlo.
Pasará por el proceso como si estuviera escribiendo un sitio web tradicional del lado del servidor.
El flujo de OAuth 2 del lado del servidor
El primer paso ocurre completamente fuera del flujo de la aplicación. El propietario del proyecto debe registrar su aplicación con cada proveedor de OAuth 2 para el que necesite iniciar sesión.
Durante este registro, proporcionan al proveedor de OAuth 2 un URI de devolución de llamada, en el que su aplicación estará disponible para recibir solicitudes. A cambio, reciben una clave de cliente y un secreto de cliente. Estos tokens se intercambian durante el proceso de autenticación para validar las solicitudes de inicio de sesión.
Los tokens se refieren a su código de servidor como el cliente. El host es el proveedor de OAuth 2. No están destinados a los clientes de su API.
El flujo comienza cuando su aplicación genera una página que incluye un botón, como "Iniciar sesión con Facebook" o "Iniciar sesión con Google+". Fundamentalmente, estos no son más que simples enlaces, cada uno de los cuales apunta a una URL como la siguiente:
https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
(Nota: se insertaron saltos de línea en el URI anterior para facilitar la lectura).
Ha proporcionado su clave de cliente y URI de redireccionamiento, pero no secretos. A cambio, le ha dicho al servidor que le gustaría un código de autenticación en respuesta y acceso a los ámbitos de 'perfil' y 'correo electrónico'. Estos ámbitos definen los permisos que solicita al usuario y limitan la autorización del token de acceso que recibe.
Una vez recibido, el navegador del usuario es dirigido a una página dinámica que controla el proveedor de OAuth 2. El proveedor de OAuth 2 verifica que el URI de devolución de llamada y la clave del cliente coincidan antes de continuar. Si lo hacen, el flujo diverge brevemente según los tokens de sesión del usuario.
Si el usuario no ha iniciado sesión actualmente en ese servicio, se le pedirá que lo haga. Una vez que ha iniciado sesión, se le presenta al usuario un cuadro de diálogo que solicita permiso para permitir que su aplicación inicie sesión.
Suponiendo que el usuario aprueba, el servidor OAuth 2 lo redirige al URI de devolución de llamada que proporcionó, incluido un código de autorización en los parámetros de consulta: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
.
El código de autorización es un token de un solo uso que expira rápidamente; inmediatamente después de su recepción, su servidor debe dar la vuelta y realizar otra solicitud al proveedor de OAuth 2, incluidos tanto el código de autorización como su secreto de cliente:
POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET
El propósito de este código de autorización es autenticar la solicitud POST anterior, pero debido a la naturaleza del flujo, debe enrutarse a través del sistema del usuario. Como tal, es intrínsecamente inseguro.
Las restricciones sobre el código de autorización (es decir, que vence rápidamente y solo se puede usar una vez) existen para mitigar el riesgo inherente de pasar una credencial de autenticación a través de un sistema que no es de confianza.
Esta llamada, realizada directamente desde su servidor al servidor del proveedor de OAuth 2, es el componente clave del proceso de inicio de sesión del lado del servidor de OAuth 2. Controlar la llamada significa que sabe que la llamada está protegida por TLS, lo que ayuda a protegerla contra ataques de escuchas telefónicas.
Incluir el código de autorización asegura que el usuario otorgó su consentimiento de forma explícita. Incluir el secreto del cliente, que nunca es visible para sus usuarios, garantiza que esta solicitud no se origine a partir de algún virus o malware en el sistema del usuario, que interceptó el código de autorización.
Si todo coincide, el servidor devuelve un token de acceso , con el que puedes realizar llamadas a ese proveedor mientras estás autenticado como usuario.
Una vez que haya recibido el token de acceso del servidor, su servidor redirige el navegador del usuario una vez más a la página de destino para los usuarios que acaban de iniciar sesión. Es común conservar el token de acceso en el caché de sesión del lado del servidor del usuario, por lo que que el servidor puede hacer llamadas al proveedor social dado cuando sea necesario.
¡El token de acceso nunca debe estar disponible para el usuario!
Hay más detalles en los que podríamos sumergirnos.
Por ejemplo, Google incluye un token de actualización que prolonga la vida útil de su token de acceso, mientras que Facebook proporciona un punto final en el que puede intercambiar tokens de acceso de corta duración por algo de mayor duración. Sin embargo, estos detalles no nos importan porque no vamos a utilizar este flujo.
Este flujo es engorroso para una API REST. Si bien puede hacer que el cliente front-end genere la página de inicio de sesión inicial y hacer que el back-end proporcione una URL de devolución de llamada, eventualmente se encontrará con un problema. Desea redirigir al usuario a la página de destino del front-end una vez que haya recibido el token de acceso, y no hay una forma REST clara de hacerlo.
Afortunadamente, hay otro flujo de OAuth 2 disponible, que funciona mucho mejor en este caso.
El flujo de OAuth 2 del lado del cliente
En este flujo, el front-end se vuelve responsable de manejar todo el proceso de OAuth 2. Por lo general, se parece al flujo del lado del servidor, con una excepción importante: los front-end viven en máquinas que los usuarios controlan, por lo que no se les puede confiar el secreto del cliente. La solución es simplemente eliminar todo ese paso del proceso.
El primer paso, como en el flujo del lado del servidor, es registrar la aplicación.
En este caso, el propietario del proyecto aún registra la aplicación, pero como una aplicación web. El proveedor de OAuth 2 seguirá proporcionando una clave de cliente , pero es posible que no proporcione ningún secreto de cliente.
El front-end proporciona al usuario un botón de inicio de sesión social, que lo dirige a una página web que controla el proveedor de OAuth 2 y solicita permiso para que nuestra aplicación acceda a ciertos aspectos del perfil del usuario.
Sin embargo, la URL se ve un poco diferente esta vez:
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
Tenga en cuenta que el parámetro response_type
esta vez en la URL es token
.
Entonces, ¿qué pasa con el URI de redirección?
Esta es simplemente cualquier dirección en el front-end que esté preparada para manejar el token de acceso de manera adecuada.
Dependiendo de la biblioteca OAuth 2 en uso, el front-end puede ejecutar temporalmente un servidor capaz de aceptar solicitudes HTTP en el dispositivo del usuario; en ese caso, la URL de redirección tiene el formato http://localhost:7862/callback/?token=TOKEN
.
Debido a que el servidor OAuth 2 devuelve una redirección HTTP después de que el usuario haya aceptado, y esta redirección es procesada por el navegador en el dispositivo del usuario, esta dirección se interpreta correctamente, lo que le da al usuario acceso al token.
Alternativamente, el front-end puede implementar directamente una página apropiada. De cualquier manera, el front-end es responsable, en este punto, de analizar los parámetros de consulta y procesar el token de acceso.
A partir de este momento, el front-end puede llamar directamente a la API del proveedor de OAuth 2 mediante el token. Pero los usuarios realmente no quieren eso; quieren acceso autenticado a su API. Todo lo que debe proporcionar el back-end es un punto final en el que el front-end puede intercambiar el token de acceso de un proveedor social por un token que otorga acceso a su API.
¿Por qué permitir esto en absoluto, dado que proporcionar el token de acceso al front-end es intrínsecamente menos seguro que el flujo del lado del servidor?

El flujo del lado del cliente permite una separación más estricta entre una API REST de back-end y un front-end orientado al usuario. No hay nada que le impida estrictamente especificar su servidor back-end como el URI de redirección; el efecto final sería una especie de flujo híbrido.
El problema es que el servidor debe generar una página adecuada para el usuario y luego devolver el control al front-end de alguna manera.
Es común en los proyectos modernos separar estrictamente las preocupaciones entre la interfaz de usuario del front-end y el back-end que maneja toda la lógica comercial. Por lo general, se comunican a través de una API JSON bien definida. Sin embargo, el flujo híbrido descrito anteriormente enturbia esa separación de preocupaciones, lo que obliga al back-end a servir una página orientada al usuario y luego diseñar algún flujo para de alguna manera devolver el control al front-end.
Permitir que el front-end maneje el token de acceso es una técnica conveniente que mantiene la separación de preocupaciones. Aumenta un poco el riesgo de un cliente comprometido, pero funciona bien, en general.
Este flujo puede parecer complicado para el front-end, y lo es, si requiere que el equipo de front-end desarrolle todo por su cuenta. Sin embargo, tanto Facebook como Google proporcionan bibliotecas que permiten que el front-end incluya botones de inicio de sesión que manejan todo el proceso con una configuración mínima.
Aquí hay una receta para el intercambio de tokens en el back-end.
Bajo el flujo del cliente, el back-end está bastante aislado del proceso OAuth 2. No se deje engañar: este no es un trabajo simple. Querrá que admita al menos las siguientes funcionalidades.
- Envíe al menos una solicitud al proveedor de OAuth 2, solo para asegurarse de que el token que proporcionó el front-end sea válido, no una cadena aleatoria arbitraria.
- Cuando el token sea válido, devuelva un token válido para su API. En caso contrario, devolver un error informativo.
- Si se trata de un usuario nuevo, cree un modelo de
User
para él y rellénelo adecuadamente. - Si se trata de un usuario para el que ya existe un modelo
User
, haga coincidir su dirección de correo electrónico para que obtenga acceso a la cuenta existente correcta en lugar de crear una nueva para el inicio de sesión social. - Actualice los detalles del perfil del usuario según lo que haya proporcionado en las redes sociales.
La buena noticia es que implementar toda esta funcionalidad en el back-end es mucho más simple de lo que podría esperar.
Aquí está la magia de cómo hacer que todo esto funcione en el back-end en solo dos docenas de líneas de código. Esto depende de la biblioteca Python Social Auth ("PSA" en adelante), por lo que deberá incluir tanto social-auth-core
como social-auth-app-django
en su requirements.txt
.
También deberá configurar la biblioteca como se documenta aquí. Tenga en cuenta que esto excluye el manejo de algunas excepciones para mayor claridad.
El código completo para este ejemplo se puede encontrar aquí.
@api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )
Solo hay un poco más que necesita ir en su configuración (código completo), y luego está todo listo:
AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )
Agrega un mapeo a esta función en tu urls.py
, ¡y listo!
¿Cómo funciona esa magia?
Python Social Auth es una pieza de maquinaria muy interesante y muy compleja. Es perfectamente feliz de manejar la autenticación y el acceso a cualquiera de varias docenas de proveedores de autenticación social, y funciona en los marcos web de Python más populares, incluidos Django, Flask, Pyramid, CherryPy y WebPy.
En su mayor parte, el código anterior es una vista basada en la función Django REST framework (DRF) muy estándar: escucha las solicitudes POST en cualquier ruta a la que lo asigne en su urls.py
y, suponiendo que envíe una solicitud en el formato que espera, luego obtiene un objeto User
, o None
.
Si obtiene un objeto User
, es del tipo de modelo que configuró en otra parte de su proyecto, que puede o no haber existido. PSA ya se encargó de validar el token, identificar si existía o no una coincidencia de usuario, crear un usuario si fuera necesario y actualizar los detalles del usuario del proveedor social.
Los detalles exactos de cómo se asigna un usuario desde el usuario del proveedor de redes sociales al suyo, y cómo se asocia con los usuarios existentes, se especifican mediante SOCIAL_AUTH_PIPELINE
definido anteriormente. Hay mucho más que aprender sobre cómo funciona todo esto, pero está fuera del alcance de esta publicación. Puedes leer más sobre esto aquí.
La clave de la magia es el decorador @psa()
en la vista, que agrega algunos miembros al objeto de request
que se pasa a su vista. El más interesante para nosotros es request.backend
(para PSA, un backend es cualquier proveedor de autenticación social).
Se eligió el back-end apropiado para nosotros y se agregó al objeto de request
en función del argumento del backend
-end de la vista, que se completa con la propia URL.
Una vez que tenga el objeto de backend
-end en la mano, está perfectamente feliz de autenticarlo contra ese proveedor, dado su código de acceso; ese es el método do_auth
. Esto, a su vez, involucra la totalidad de SOCIAL_AUTH_PIPELINE
desde su archivo de configuración.
La canalización puede hacer algunas cosas bastante poderosas si la amplía, aunque ya hace todo lo que necesita con nada más que su funcionalidad integrada predeterminada.
Después de eso, vuelve al código DRF normal: si obtuvo un objeto User
válido, puede devolver fácilmente un token de API apropiado. Si no recuperó un objeto User
válido, es fácil generar un error.
Una desventaja de esta técnica es que, si bien es relativamente simple devolver errores si ocurren, es difícil obtener mucha información sobre lo que salió mal específicamente. PSA se traga cualquier detalle que el servidor pueda haber devuelto sobre cuál era el problema.
Por otra parte, está en la naturaleza de los sistemas de autenticación bien diseñados ser bastante opacos sobre las fuentes de error. Si una aplicación alguna vez le dice a un usuario "Contraseña no válida" después de un intento de inicio de sesión, eso equivale a decir "¡Felicitaciones! Has adivinado un nombre de usuario válido”.
¿Por qué no simplemente rodar el tuyo?
En una palabra: extensibilidad. Muy pocos proveedores de OAuth 2 social requieren o devuelven exactamente la misma información en sus llamadas API exactamente de la misma manera. Sin embargo, hay todo tipo de casos especiales y excepciones.
Agregar un nuevo proveedor social una vez que ya haya configurado un PSA es una cuestión de unas pocas líneas de configuración en sus archivos de configuración. No tienes que ajustar ningún código en absoluto. PSA abstrae todo eso, para que pueda concentrarse en su propia aplicación.
¿Cómo diablos pruebo esto?
¡Buena pregunta! unittest.mock
no es adecuado para burlarse de las llamadas API ocultas bajo una capa de abstracción en lo profundo de una biblioteca; solo descubrir el camino preciso para burlarse requeriría un esfuerzo sustancial.
En cambio, debido a que PSA está construido sobre la biblioteca de Solicitudes, utiliza la excelente biblioteca de Respuestas para simular los proveedores en el nivel HTTP.
Una discusión completa de las pruebas está más allá del alcance de este artículo, pero aquí se incluye una muestra de nuestras pruebas. Las funciones particulares a tener en cuenta son el administrador de contexto SocialAuthTests
mocked
Deje que PSA haga el trabajo pesado.
El proceso de OAuth2 es detallado y complicado con mucha complejidad inherente. Afortunadamente, es posible eludir gran parte de esa complejidad al incorporar una biblioteca dedicada a manejarla de la manera más sencilla posible.
Python Social Auth hace un gran trabajo en eso. Hemos demostrado una vista de Django/DRF que utiliza el flujo OAuth2 implícito del lado del cliente para obtener una creación y coincidencia de usuarios perfecta en solo 25 líneas de código. Eso no está mal.