Cómo crear un proveedor de autenticación en Drupal 8

Author Top
enzo

Algunas semanas atrás explique Cómo crear un recurso REST en Drupal 8 y como mostré en esa entrada de blog se utilizo el método de autenticación Basic Auth como se puede ver en la siguiente imagen.

Actualmente es el único proveedor de autenticación disponibles para su uso en un escenario CORS. Si quieres saber más sobre CORS se puede ver el video Entendiendo Drupal 8 Rest y Backbone.Drupal para hacer llamados CORS.

¿Pero qué pasa si el Basic Auth no se ajusta a nuestras necesidades? Bueno hoy te te mostraré cómo crear un proveedor de autenticación personalizado.

1. El requerimiento

Mi petición imaginaria será crear un proveedor de autenticación sin la validación del usuario, que significa acceso como usuario Anónimo, pero que requiere validar la fuente de la solicitud para comprobar que este en la lista direcciones IP habilitadas para acceder a nuestros recursos REST.

2. Crear un modulo

Voy a omitir la explicación acerca de cómo crear el módulo ip_consumer_auth  en Drupal 8, porque se puede generar con el proyecto Drupal console ejecutando el siguiente comando.

$ drupal generate:module

Después de crear el módulo con la consola, utilizaremos de nuevo la consola para crear un Formulario de Configuración nuestra direcciones IP permitidas mediante el siguiente comando.

$ drupal generate:form:config

Ahora debemos agregar un campo Textarea al  formulario para almacenar las direcciones IP validas. La implementación del método buildForm seré similar a siguiente fragmento de código.

/**
 * {@inheritdoc}
 */
public function buildForm(array $form, FormStateInterface $form_state) {
  $config = $this->config('ip_consumer_auth.consumers_form_config');
  $form['allowed_ip_consumers'] = [
    '#type' => 'textarea',
    '#title' => $this->t('Allowed IP Consumers'),
    '#description' => $this->t('Place IP addresses on separate lines'),
    '#default_value' => $config->get('allowed_ip_consumers'),
  ];
  return parent::buildForm($form, $form_state);
}

La clase de formulario estará ubicado en ip_consumer_auth/src/Form/ConsumersForm.php

El formulario para ingresar las direcciones IP permitidas debería lucir similar a la siguiente image.

El código para guardar los valores son generados por Drupal console, de la misma manera que se crea el enrutamiento. Asegúrese de cambiar los valores a su conveniencia, después de generar el formulario.

3. Crear Proveedor de Autenticación.

Antes de comenzar con el código para nuestro Proveedor de Autenticación tenemos que informar a Drupal 8 de la existencia de nuestro proveedor de autenticación personalizado, para ello agregamos un nuevo archivo en nuestro módulo con el nombre ip_consumer_auth.service.yml porque mi nombre de módulo es ip_consumer_auth.

El contenido de este archivo sera similar al siguiente.

services:
  authentication.ip_consumer_auth:
    class: Drupal\ip_consumer_auth\Authentication\Provider\IPConsumerAuth
    arguments: ['@config.factory', '@entity.manager']
    tags:
      - { name: authentication_provider, priority: 100 }

El Discover Services encontrara este archivo y hará el registro de nuestra clase de proveedor de autenticación Drupal\ip_consumer_auth\Authentication\Provider\IPConsumerAuth y preparara los elementos para enviar al constructor.

Con la anterior sentencia no es necesario implementar el método de create en nuestra clase, por que Discover enviar los arguments usando Inyección de Dependencias.

Al final se define la prioridad, este valor definirá el orden de ejecución de los proveedor de autenticación si se habilitaron multiples proveedores.

4. Implementar Clase Proveedor de Autenticación

Nuestra clase IPConsumerAuth debe implementar la interfaz AuthenticationProviderInterface como se puede ver en el siguiente fragmento.

/**
 * IP Consumer authentication provider.
 */
class IPConsumerAuth implements AuthenticationProviderInterface {
}

4.1 Librerías

El proveedor de autenticación requiere algunas bibliotecas y debemos informar al AutoLoader dónde están estas bibliotecas, déjenme mostrarte la lista completa.

namespace Drupal\ip_consumer_auth\Authentication\Provider;

use \Drupal\Component\Utility\String;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

4.2 Implementar método Applies

Incluso si el proveedor de autenticación está activado para algún recurso REST se requiere una validación para confirmar que el proveedor de autenticación aplica a la petición actual.

En nuestro caso, si nuestro proveedor de autenticación se habilitó no agregaremos ninguna validación adicional como se puede ver en la siguiente aplicación.

/**
 * {@inheritdoc}
 */
public function applies(Request $request) {
  // If Authentication Provider is enabled always apply
  return TRUE;
}

4.3 Implementar método Authenticate

Ahora tenemos que aplicar la lógica a ejecutar para validar la solicitud, compruebe el siguiente fragmento de código.

/**
 * {@inheritdoc}
 */
public function authenticate(Request $request) {
  $allowed_ip_consumers = $this->configFactory->get('ip_consumer_auth.consumers_form_config')->get('allowed_ip_consumers');
  $ips = array_map('trim', explode( "\n", $allowed_ip_consumers));
  $consumer_ip = $request->getClientIp(TRUE);
  if (in_array($consumer_ip, $ips)) {
    // Return Anonymous user
    return $this->entityManager->getStorage('user')->load(0);
  }
  else{
    throw new AccessDeniedHttpException();
    return null;
  }
}

En la implementación anterior se uso el configFactory para obtener la información almacenada sobre las direcciones IP permitidas ingresadas mediante el Formulario de Configuración creado antes.

El método de autenticación recibe un objeto Request definido  el componente de Symfony HttpFoundation.

Utilizando el método getClientIp() obtenemos la IP del Consumidor.

Usando las funciones de PHP explode, array_map y in_array para determinar si los consumidores IP pertenecen a lista Permitida de direcciones IP( Sé que tal vez con una expresión regular será más eficiente pero realmente so muy malo con Regex).

Si pasa la validación retornara un objeto tipo User para el usuario Anónimo, si falla una excepción Acceso Denied es lanzada.

4.4 Implementar método HandleException

Si la IP no está en la lista de direccionesIP permitidas una excepción es un lanzada, utilizando el método HandleException tenemos la opción de interceptar la excepción y procesarla para producir cualquier salida deseada.

Permítanme compartir con ustedes mi implementación.

/**
   * {@inheritdoc}
   */
  public function cleanup(Request $request) {}

  /**
   * {@inheritdoc}
   */
  public function handleException(GetResponseForExceptionEvent $event) {
    $exception = $event->getException();
    if ($exception instanceof AccessDeniedHttpException) {
      $event->setException(new UnauthorizedHttpException('Invalid consumer origin.', $exception));
      return TRUE;
    }
    return FALSE;
  }

Como se puede ver no es complejo, pero es sólo una idea superficial de lo que se puede hacer con este método.

5. Uso del Proveedor de Autenticación

Después de crear nuestro módulo que implementa un nuevo proveedor de autenticación personalizada y activarlo, estamos listos para empezar a utilizarlo.

Imaginemos que  instalamos el módulo Entity Rest Extra y deseamos utilizar nuestro nuevo proveedor de autenticación.

Utilizando el módulo Rest UI (recomiendo usar la versión de git hasta que Drupal 8 tenga su primera versión oficial) habilitamos el recurso REST y habilitamos nuestros nuevo proveedor de autenticación, como se puede ver en la siguiente imagen.

5.1 Access Denied

Ahora, si se intenta acceder a través de CORS al recurso REST http://example.com/bundles/node obtendrá un error 403 Forbidden porque las IP permitidas no están definidas

5.2 Unauthorized

Después de habilitar alguna dirección IP y volver a intentar accesar http://example.com/bundles/node obtendrá un error 401 Unauthorized.

Si ese error no tiene ninguna lógica para usted, permítame explicarle, recordemos que en nuestro proveedor de autenticación no tenemos información acerca del usuario, por lo que si la solicitud pasa la validación de IP Consumidor retornamos un usuario anónimo.

Cuando habilitamos algún recurso REST en Drupal 8 un nuevo conjunto de permisos se crea, en nuestro caso tenemos que asignar el permiso Access GET on Bundles by entities resource para el usuario Anónimo como se puede ver en la siguiente imagen.

5.3 Nuevamente Access Denied 

Bueno, tal vez en este momento estas enojado conmigo, porque ahora el recurso REST retorna nuevamente un error 403 Forbidden, pero en este momento la culpa no es causado por el proveedor de autenticación o del permiso del recurso REST.

Ahora el error está relacionado con recurso REST en sí mismo . Sí  comprobar el código del módulo Entity Rest Extra encontrara  que el permiso  Administer content types es necesario para acceder a este recurso REST.

6. Bono

bueno usted puede quejarse de nadie le informo a usted que el módulo tiene validación permisos propios o los permisos REST, lo cual puede ser cierto.

Si desea obtener más información sobre un router específico de Drupal 8 puede utilizar la Drupal Console.

Lo primero que tienes que hacer es determinar el ID del router, podemos usar URL canónica del recurso REST como se puede ver en el siguiente comando.

 $ console router:debug | grep bundles/{entity}
 rest.entity_bundles.GET.json                             /bundles/{entity}

Ahora podemos obtener más información sobre router mediante el siguiente comando.

$ console router:debug rest.entity_bundles.GET.json
 Route name                   Options
 rest.entity_bundles.GET.json
  + Pattern                   /bundles/{entity}
  + Defaults
   - _controller              Drupal\rest\RequestHandler::handle
   - _plugin                  entity_bundles
  + Options
   - compiler_class           \Drupal\Core\Routing\RouteCompiler
   - _access_mode             ANY
   - 0                        ip_consumer_auth
   - 0                        access_check.permission

Como puedes ver lo último que se ejecuta es access_check.permission generando el error 401. Tal vez en el futuro los permisos internos de los recursos necesarios se incluirán.

Si desea ver una implementación completa del proveedor de autenticación puede revisar el proyecto IP Consumer Auth.

Espero que está entrada de blog haya sido útil.