— 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\ClientTo 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\ClientGuzzleHttp\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\ServiceClientFactoryNext 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
MemberProfileControllerrouting 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.jsonfile
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
\nin it. You'll need to replace the\nwith actual new lines in thesettings.local.phpfile.
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.