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 create an Authentication Provider without user validation, that means access as an Anonymous user, but we require to validate the source of the 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 creating 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 look similar to the 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 look 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 generating the form.
Create Authentication Provider
Before to start with the code for our Authentication 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 the 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 requires 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 a 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 belongs to Allowed IP consumer. I know maybe with a regex will be more efficient but I really suck in Regex.
If validation pass I return an Account object for the Anonymous user, if fail an Access Denied Exception is throw.
Implement method handleException
If the IP wasn’t on the allowed list of IP Consumer an exception is thrown, 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 creating our module with our custom Authentication Provider and enable it, we are ready to start to use.
Let's 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 the 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 endpoint http://example.com/bundles/node you will get an error 403 Forbidden because the allowed IP consumers weren’t defined yet.
Unauthorized
After enabling 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 passes 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.
The 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 to see a complete implementation of Authentication provider you clone the project IP Consumer Auth
I hope you found this blog entry useful.