Authenticated Calls to the AtroPIM API with Guzzle PHP

How to use Guzzle middleware to send authenticated calls to an API and retry if the token got invalidated.

AtroPIM is an pretty clever open source PIM system, that can be extended by commercial plugins and services.

The cool thing is that it doesn't come with much technical debt. PHP and MySQL on a VPS with Ubuntu. That's it. With some tricks it runs on an Ionos Managed Server, but that's not really recommended. Also, the company hosts it for you.

Anyway if you want to use your product data outside of AtroPIM you need to call their API.

Here comes the code for how to query the AtroPIM API while authenticate the first call and use a cached token for all subsequent calls. If that's too specific it serves at least as a reminder on how to deal with authentication in Guzzle and retry to authenticate in case the token gets invalidated.

<?php

$stack = \GuzzleHttp\HandlerStack::create();

$stack->push(\GuzzleHttp\Middleware::retry(function ($retry, $request, $value, $reason) {
  if ($value && $value->getStatusCode() === 401) {
    cache()->flush();
    return true;
  }

  if ($value !== null) {
    return false;
  }

  return $retry < 5;
}));

$stack->push(new AuthenticationHandler(
  'api_user',
  'api_password',
  cache('pim') //instance of a caching method
));

$this->client = new \GuzzleHttp\Client([
  'base_uri' => 'https://pim.com/api/v1/',
  'handler' => $stack,
]);

AuthenticationHandler.php

<?php

use Psr\Http\Message\RequestInterface;

class AuthenticationHandler
{

  private $username;

  private $password;

  private $token_name = 'access_token';

  private $cache;

  public function __construct($username, $password, $cache = null)
  {
    $this->username = $username;
    $this->password = $password;
    $this->cache = $cache;
  }

  public function __invoke(callable $handler)
  {
    return function (RequestInterface $request, array $options) use ($handler) {
      if (is_null($token = $this->cache->get($this->token_name))) {
        $response = $this->apiAuth($options['base_uri']);
        $this->cache->set($this->token_name, $token = $response);
      }

      return $handler(
        $request->withAddedHeader('Authorization-Token', $token),
        $options
      );
    };
  }

  public function apiAuth(string $base_uri)
  {
    $client = new \GuzzleHttp\Client([
      'base_uri' => $base_uri
    ]);

    // https://github.com/atrocore/atrocore-docs/blob/master/en/developer-guide/rest-api.md
    $response = $client->get('App/user', [
      'auth' => [$this->username, $this->password],
      'headers' => [
        // token lives forever
        'Authorization-Token-Lifetime' => 0,
        'Authorization-Token-Idletime' => 0,
      ],
    ]);

    $token = json_decode($response->getBody());

    return base64_encode($this->username . ':' . $token['user']['token']);
  }
}

As documented, if Authorization-Token-Lifetime is set to 0 it's valid forever and needs to authenticate only once. In case the token gets invalidated and the API answers with a 401 status code, Middleware::retry catches the 401 code, slushes the cache and tries to load again to retrieve the endpoint, along with the new token.

Inspiration