— Technology, Drupal, PHP, GuzzleHttp — 2 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:
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:
GuzzleHttp\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:
Factory
which will create the GuzzleHttp\Client
GuzzleHttp\Client
serviceDataService
wrapping Drupal logging functionalityMemberService
, etc. that will be used to provide #getContactInfo()
specific functionsAt this point, we'll just get the Service and Factory configured and available:
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
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 middleware1class 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:
1class MemberProfileController extends ControllerBase {2
3 /**4 * @var \GuzzleHttp\Client5 */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.
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:
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.
To make the configuration available, we need to pop some of these items into the settings.local.php
file (or .env file if preferred).
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 thesettings.local.php
file.
Now that we have the configuration available, we can implement the
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:
1class Authentication {2 // Constructor and field definition3 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->keyId19 ];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:
GoogleAuthenticationTokenVerifier
using the supplied information from the json
fileBut at this point we should be in the position to start making authenticated requests.