Eduardo Garcia bio photo

enzo

Eduardo Garcia

Knowmad by definition

Location: Costa Rica

Twitter Facebook  QQ交谈 Google+ Github LinkedIn Feed

Some weeks ago I show you How to create a Rest Resource in Drupal 8 and as I explain in that post we use the Authentication Provider Basic Auth as you can see in the following image.

Right now is the unique Authentication Provider available to use in a CORS scenery. If you want to read more about CORS you can read the blog entry What is Cross-Origin Resource Sharing (CORS).

But what about if Basic Auth doesn't fit with your needs, well I will show you how to create a custom Authentication Provider.

The Requirement

My imaginary request will be create an Authentication Provider without user validation, that means access as Anonymous user, but we require to validate the source of request to check against an IP White List enabled to access our REST resources.

Create a Module

I will skip the explanation about how to create the Module ip_consumer_auth in Drupal 8 because could be generated using the project Drupal Console executing the following command.

$ drupal generate:module

After create the module with the console, we will use the console again to create a Form Configuration to create our IP White List using the following command.

$ drupal generate:form:config

Now we must add a Textarea field to the form to store allowed IP Address. The implementation of method buildForm will looks similar to following snippet.

/**
 * {@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);
}

The form class will be located at ip_consumer_auth/src/Form/ConsumersForm.php

The layout to enter allowed IP consumer will looks similar to this image

The code to save that values are generated by Drupal Console, in the same way the Routing is created. Be sure you change the values to your convenience after generate the form.

Create Authentication Provider

Before to start with code for our Authetication Provider we need to inform to Drupal 8 the existence of our custom Authentication Provider, to do that we add a new file in our module named ip_consumer_auth.service.yml because my module name is ip_consumer_auth.

Let me show you the content of that file

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

The discover for services will find this file and registering our Authentication Provider Class Drupal\ip_consumer_auth\Authentication\Provider\IPConsumerAuth and prepare the elements to send to constructor.

With the sentence above we don't have to implement the method create in our class, because the Discover send the parameters using Dependency Injection.

At the end we define the priority, this value will define the execution order if multiple Authentication Provider were enabled.

Implement Class Authentication Provider IPConsumerAuth

Our class IPConsumerAuth must implement the interface AuthenticationProviderInterface as you can see in the following snippet.

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

Libraries

The Authentication Provider require some libraries and we must to inform to AutoLoader where are these libraries, let me show the complete list.

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;

Implement method applies

Even if the Authentication Provider is enabled for some REST resource is required a validation to confirm the Authentication Provider apply for current Request.

In our case if our Authentication Provider was enabled we will add any extra validation as you can see in the following implementation.

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

Implement method authenticate

Now we have to implement the logic to execute to validate the Request, check the following snippet.

/**
 * {@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;
  }
}

In the implementation above I use the configFactory to get the information stored about allowed IP Consumer using the Settings form created before.

The authenticate method receive an Request object defined in The HttpFoundation Component of Symfony.

Using the Request method getClientIp we get the IP of Consumer.

I used the PHP functions explode, array_map and in_array to determine if IP consumer belong to Allowed IP consumer. I know maybe with a regex will be more efficient but I really sucks in Regex.

If validation pass I return an Account object for Anonymous user, if fail an Access Denied Exception is throw.

Implement method handleException

If the IP wasn't in the allowed list of IP Consumer an exception is throw, using the method handleException we have the option to intercept and process to produce any output desired.

Let me share with you my implementation

  /**
   * {@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;
  }

As you can see is not to complex, but is just an idea above what you can do with this method.

Usage

After create our module with our custom Authentication Provider and enable it, we are ready to start to use.

Lets imagine you install the module Entity Rest Extra and we will use our Authentication Provider.

Using the Rest UI(I recommend to use the git version until Drupal 8 get a first release) module we enable the REST Resource and enable our Custom Authentication Provider, as you can see in the following image.

Access Denied

Now if you try to access via CORS the REST end point http://example.com/bundles/node you will get an error 403 Forbidden because the allowed IP consumers weren't defined yet.

Unauthorized

After enable your IP consumer and try again http://example.com/bundles/node you will get an error 401 Unauthorized

If that error doesn't have any logic for you let me explain, remember in our Authentication Provider we don't have information about any user, so if the request pass the validation of IP Consumer we return an Anonymous user.

When we enable any REST Resource in Drupal 8 a new set of permissions is created, in our case we have to assign the permission Access GET on Bundles by entities resource to Anonymous user as you can see in the following image.

Access Denied AGAIN

Well maybe at this point you get MAD with me, because now you get again an error 403 Forbidden, but at this time the fault is not caused by Authentication Provider or by Rest permission itself.

The error now is related with REST Resource itself is you check the code of module Entity Rest Extra you will found the permission Administer content types is required to access this Resource.

Bonus

Well you can complain about nobody inform to you that module has own permissions validation or the REST permissions, well that could be true.

If you want to get more information about a specific Drupal 8 router you can use the Drupal Console.

First thing you have to do is determine the Router id, we can you the canonical URL of REST resource as you can see in the following command.

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

Now we can get more information about router using the following command.

 $ 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
  

As you can see the last thing executed is access_check.permission generating the error 401. Maybe in the future the internal permissions in resourced will be included.

If you want see a complete implementation of Authentication provider you clone the project IP Consumer Auth

I hope you found this blog entry useful.


Comments

comments powered by Disqus