Writing a custom authenticator in Drupal 8

Drupal 8 allows module developers to write their own customized authentication schemes. In this post, we shall see how we create one. Let's take a hypothetical custom authentication mechanism called the token authentication mechanism. It works like this:

The site administrator has a limited set of auto generated tokens. They issue these tokens to users who want to access the site's resources. These resources can only be accessed by giving the correct token as a part of the URL parameter, like my-page?token=ABCXYZ.

First, let's generate a module to hold our token authenticator.

drupal generate:module

 Enter the new module name:
 > Token Authentication

 Enter the module machine name [token_authentication]:
 > token_auth

 Enter the module Path [/modules/custom]:
 > 

 Enter module description [My Awesome Module]:
 > Token based custom authenticator

 Enter package name [Custom]:
 > Examples

 Enter Drupal Core version [8.x]:
 > 

 Do you want to generate a .module file (yes/no) [yes]:
 > no

 Define module as feature (yes/no) [no]:
 > no

 Do you want to add a composer.json file to your module (yes/no) [yes]:
 > no

 Would you like to add module dependencies (yes/no) [no]:
 > no


 Do you confirm generation? (yes/no) [yes]:
 > 

Generated or updated files
 Site path: /var/www/html
 1 - modules/custom/token_auth/token_auth.info.yml

Next, we need a way to store and retrieve access tokens, preferably with UI. Config entities fit this bill, so let's go ahead and create a config entity called auth_token.

drupal generate:entity:config
 Enter the module name [email_management]:
 > token_auth

 Enter the class of your new config entity [DefaultEntity]:
 > AuthToken

 Enter the name of your new config entity [auth_token]:
 > 

 Enter the label of your new config entity [Auth token]:
 > Authentication Token 

 Enter the base-path for the config entity routes [/admin/structure]:
 > /admin/config/system

Generated or updated files
 Site path: /var/www/html
 1 - modules/custom/token_auth/config/schema/auth_token.schema.yml
 2 - modules/custom/token_auth/token_auth.links.menu.yml
 3 - modules/custom/token_auth/token_auth.links.action.yml
 4 - modules/custom/token_auth/src/Entity/AuthTokenInterface.php
 5 - modules/custom/token_auth/src/Entity/AuthToken.php
 6 - modules/custom/token_auth/src/AuthTokenHtmlRouteProvider.php
 7 - modules/custom/token_auth/src/Form/AuthTokenForm.php
 8 - modules/custom/token_auth/src/Form/AuthTokenDeleteForm.php
 9 - modules/custom/token_auth/src/AuthTokenListBuilder.php

We shall polish the config entity a bit to include 2 new properties, token to hold the token, a boolean flag enabled to indicate whether the token is enabled or not.

auth_token.schema.yml

token:
  type: string
  label: 'Auth Token'
enabled:
  type: boolean
  label: 'Enabled'

The token is a readonly property which is autogenerated and set at the time of creating a new entity instance.

AuthTokenForm.php

if($auth_token->isNew()) {
  $auth_token->set("token", Crypt::randomBytesBase64());
}
$status = $auth_token->save();

Let's create a custom authentication provider to implement token based authentication.

drupal generate:authentication:provider
 Enter the module name [email_management]:
 > token_auth

 Authentication Provider class [DefaultAuthenticationProvider]:
 > TokenAuth

 Provider ID [token_auth]:
 > 


 Do you confirm generation? (yes/no) [yes]:
 > yes

Generated or updated files
 Site path: /var/www/html
 1 - modules/custom/token_auth/src/Authentication/Provider/TokenAuth.php
 2 - modules/custom/token_auth/token_auth.services.yml

Auth token config entities

The authentication scheme here is to allow only logged in users to view a page, provided they give a valid and enabled token as a part of the URL. This functionality partly overlaps with the cookie authentication provider which ships as a part of core. Hence, this can be built on top the cookie based authentication scheme. For any new authentication provider, we have to implement 2 functions, applies() and authenticate(). The former checks if the request has appropriate credentials needed to authenticate a request, like request headers or tokens. The latter returns a user object pertaining to the credentials.

This is how both functions play out in lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php.

public function onKernelRequestAuthenticate(GetResponseEvent $event) {
  if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
    $request = $event->getRequest();
    if ($this->authenticationProvider->applies($request)) {
      $account = $this->authenticationProvider->authenticate($request);
      if ($account) {
        $this->accountProxy->setAccount($account);
        return;
      }
    }
    ...

Our authentication works exactly like cookie based authentication, with an extra check on the given token. So, we override the Cookie authentication provider implementation.

public function applies(Request $request) {
  $token = $request->query->get('token');
  return parent::applies($request) && $this->isCorrectToken($token);
}

The isCorrectToken() function checks if the given token is valid and enabled against all valid tokens in the system.

protected function isCorrectToken($tok) {
  $query = \Drupal::entityQuery('auth_token')
    ->condition('enabled', TRUE);
  $token_ids = $query->execute();
  $tokens = entity_load_multiple('auth_token', $token_ids);
  foreach($tokens as $token) {
    if($token->token() == $tok) {
      return TRUE;
    }
  }
  return FALSE;
}

Our authentication provider service looks like this:

services:
  authentication.token_auth:
    class: Drupal\token_auth\Authentication\Provider\TokenAuth
    arguments: ['@session_configuration', '@database']
    tags:
      - { name: authentication_provider, provider_id: token_auth, priority: 100 }

Now, let's effect this new authentication provider onto a route which we created earlier.

myroute.greeting_controller_greeting:
  path: 'hello/{name}'
  options:
    _auth: [ 'token_auth' ]
  defaults:
    _controller: '\Drupal\myroute\Controller\GreetingController::greeting'
    _title: 'Greeting'
  requirements:
    _permission: 'access content'
    _user_is_logged_in: 'TRUE'
    name: '[a-zA-z ]+'

You might note 2 important changes here. First, we explicitly specify the authentication scheme for this route as token_auth. Second, we enforce a rule saying only logged in users can see this route using the _user_is_logged_in mandate.

Rebuild the cache(make sure the token_auth module is enabled before that) and hit the above route(as a logged in user), first without the token parameter, as /hello/foo. You should get the access denied error.

Access denied

Now, try with the token parameter, hello/foo?token=771iKzLs4UU8aYkOF1-TkRUvaE3P_IBqeZZl6x91D78.

Successful token authentication

The above code can be checked out here under the tag custom-auth.

$ git clone git@github.com:drupal8book/token_auth.git
$ cd token_auth
$ git checkout -f custom-auth