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
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.
Now, try with the token parameter, hello/foo?token=771iKzLs4UU8aYkOF1-TkRUvaE3P_IBqeZZl6x91D78
.
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
[promo name=d8book]