Skip to content
Ken Davidson

Drupal Custom Service w/ Authentication

Technology, Drupal, PHP, GuzzleHttp2 min read

Since I've had the opportunity (for lack of a better word) to continue down the Drupal rabbit hole - the next requirements of our project required that we have Drupal communciating with our back end services (Java). Our application is based around Google OAuth and therefore it made sense to implement:

  • A custom Http client on Drupal
  • Using Google Service Accounts as authentication
  • Performing the appropriate validation on the back end

Custom Drupal Client

As much as I absolutely love Drupal, the Drupal 8 Service configuration and Injection Container was actually a delight to work with. I found a number of great articles on creating an Http Client using the built in GuzzleHttp\Client library.

There are a number of different approaches that can be taken here:

  1. Implementing a Factory to provide a standard GuzzleHttp\Client
  2. Implementing a custom client

To give an idea of the differences, it's all dependent on how much (or little) custom functionality we need to provide. The way I look at it is that I'll provide a number of levels:

  • The Factory which will create the GuzzleHttp\Client
  • The GuzzleHttp\Client service
  • A standard data wrapping service DataService wrapping Drupal logging functionality
  • Any number of added services MemberService, etc. that will be used to provide #getContactInfo() specific functions

Module Service YML File

At this point, we'll just get the Service and Factory configured and available:

module.services.yml
1module.service_client:
2 class: GuzzleHttp\Client
3 factory: module.service_client_factory:get
4 module.service_client_factory:
5 class: Drupal\module\Services\ServiceClientFactory

Implementing the Factory

Next we'll need to implement the factory, the primary methods are:

  • #get() which returns the simple new Client()
  • #handler_stack() which we use to build the appropriate middleware
ServiceClientFactory
1class ServiceClientFactory {
2 function get() {
3 $config = [
4 'base_uri' => Settings::get('client_uri'),
5 'handler' => $this->handler_stack()
6 ];
7
8 return new Client($config);
9 }
10
11 function handler_stack() {
12 $stack = HandlerStack::create();
13 $stack->push(new Authentication(Settings::get('client_email'), Settings::get('client_pkey'), Settings::get('client_key')));
14 return $stack;
15 }
16}

At this point we are able to inject the basic client into any Service(s) or Controllers(s) using the standard injection code:

Member
1class MemberProfileController extends ControllerBase {
2
3 /**
4 * @var \GuzzleHttp\Client
5 */
6 protected $client;
7
8 public function __construct(Client $client) {
9 $this->Client = $client;
10 }
11
12 public static function create(ContainerInterface $container) {
13 return new static(
14 $container->get('module.service_client')
15 );
16 }
17
18 public function memberProfileTitle() {
19 return "Member Profile {$this->currentUser()->getAccountName()}";
20 }
21
22 public function memberProfile($userId) {
23 $memberProfile = $this->client->get("members/#{$this->currentUser()->memberNumber()}/profile");
24 return [
25 '#markup' => "This is the member profile {dump($memberProfile)}"
26 ];
27 }
28}

At this point with the appropriate configuration for the MemberProfileController routing we should start seeing some errors!! We still need authentication setup.

Google Authentication

We're using Google Service Accounts for this, but essentially any authentication mechanism will work. To get up and running with Google Service Accounts we can jump over to https://cloud.google.com/iam/docs/understanding-service-accounts and get started.

Magic happens and we download our service-account.json file

The json file you get back will look like this:

Service
1{
2 "type": "service_account",
3 "project_id": "project_id",
4 "private_key_id": "private_key_id",
5 "private_key": "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n",
6 "client_email": "client@service.google.com",
7 "client_id": "client_id",
8 "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9 "token_uri": "https://oauth2.googleapis.com/token",
10 "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11 "client_x509_cert_url": "https://custom_public_key_url"
12}

In order to process the JWT I started using the firebase/php-jwt library.

Drupal Settings

To make the configuration available, we need to pop some of these items into the settings.local.php file (or .env file if preferred).

settings.local.php
1$settings['client_uri'] = 'http://localhost:8080/api/';
2$settings['client_email'] = 'client@service.google.com';
3$settings['client_pkey'] = '-----BEGIN PRIVATE KEY-----=
4-----END PRIVATE KEY-----';
5$settings['client_key'] = 'private_key_id';

One important thing to note, firebase/php-jwt does not like the private key with \n in it. You'll need to replace the \n with actual new lines in the settings.local.php file.

Now that we have the configuration available, we can implement the

Authentication Middleware

The authentication middleware is solely responsible for applying the appropriate Authentication: Bearer <JWT> header to each of our requests. We can see that there are two functions #header() and #body() responsible for generating the appropriate arrays, which is then generated using JWT::encode() and assigned to the Authentication header:

Authentication
1class Authentication {
2 // Constructor and field definition
3
4 public function __invoke(callable $handler) {
5 return function(RequestInterface $request, array $options = []) use ($handler) {
6 $body = $this->body();
7 $jwt = JWT::encode($body, $this->privateKey, "RS256", $this->keyId, $this->headers());
8 $request = $request->withHeader("Authentication", "Bearer: {$jwt});
9
10 return $handler($request, $options);
11 };
12 }
13
14 private function headers() {
15 return [
16 "alg" => "RS256",
17 "typ" => "JWT",
18 "kid" => $this->keyId
19 ];
20 }
21
22 private function body() {
23 $issuedAt = new DateTimeImmutable();
24 return [
25 "iss" => $this->email,
26 "sub" => $this->email,
27 "aud" => "custom audience here",
28 "iat" => $issuedAt->getTimestamp(),
29 "exp" => $issuedAt->modify('+1 hour')->getTimestamp()
30 ];
31 }
32}

At this point you should be able to open up your configured url /members/12352/profile and see the MemberProfileController firing requests off to the service. There are still a number of features left to implement:

  • The backend must implement the appropriate GoogleAuthenticationTokenVerifier using the supplied information from the json file
  • Logging and error handling should be implemented in the Client, specific to Drupal services

But at this point we should be in the position to start making authenticated requests.

© 2023 by Ken Davidson. All rights reserved.