Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code flow authentication #30

Merged
merged 29 commits into from
Apr 7, 2023
Merged

Code flow authentication #30

merged 29 commits into from
Apr 7, 2023

Conversation

robvanaarle
Copy link
Contributor

@robvanaarle robvanaarle commented Mar 10, 2023

Toevoegen van de Code flow Authenticatie aan de Client. Hierbij het ook mogelijk maken voor de huidige (Client Credentials flow) om een token te hergebruiken.

Hiervoor ook de documentatie aangepast. Dat legt goed uit wat er nu mogelijk is, dus hieronder het relevante hoofdstuk van de nieuwe mogelijkheden. Voorheen bestond dit hoofdstuk alleen uit de eerste 2 PHP blokken met uitleg, de rest is dus toegevoegd in deze PR.

Usage

Create an instance of the client and authenticate using the Client Credentials flow

$client = new \Picqer\BolRetailerV8\Client();
$client->authenticateByClientCredentials('your-client-id', 'your-client-secret');

Then you can get the first page of open orders by calling the getOrders() method on the client

$reducedOrders = $client->getOrders();

foreach ($reducedOrders as $reducedOrder) {
    echo 'hello, I am order ' . $reducedOrder->orderId . PHP_EOL;
}

To save requests to Bol.com, you may reuse the access token:

$accessToken = ... // your implementation of getting the access token from the storage

$client = new \Picqer\BolRetailerV8\Client();
$client->setAccessToken($accessToken);

$client->setAccessTokenExpiredCallback(function(\Picqer\BolRetailerV8\Client $client) {
  // Called at the beginning of a request to the Retailer API when the token was expired or non-existent.
  
  $client->authenticateByClientCredentials('your-client-id', 'your-client-secret'); // retrieves a new access token
  $accessToken = $client->getAccessToken();
  ... // store $accessToken for future use
});

Code flow Authentication

When authenticating using the Code flow, after receiving and validating the shortcode on your callback uri, you need to retrieve the first access and refresh token:

$client = new \Picqer\BolRetailerV8\Client();

$refreshToken = $client->authenticateByAuthorizationCode('your-client-id', 'your-client-secret', 'received-shortcode', 'callback-uri');
$accessToken = $client->getAccessToken();
... // store $accessToken and $refreshToken for future use

$orders = $client->getOrders();

The access token needs to be (re)used to make requests to the Retailer API.

$client = new \Picqer\BolRetailerV8\Client();

$accessToken = ... // your implementation of getting the access token from the storage
$client->setAccessToken($accessToken);

$orders = $client->getOrders();

The access token code is valid for a limited amount of time (600 seconds at time of writing), so it needs to be refreshed regularly using the refresh token:

$client = new \Picqer\BolRetailerV8\Client();

$accessToken = ... // your implementation of getting the access token from the storage
$client->setAccessToken($accessToken);
$client->setAccessTokenExpiredCallback(function(\Picqer\BolRetailerV8\Client $client) {
  // Called at the beginning of a request to the Retailer API when the token was expired.
  
  // This callback can attempt to refresh the access token. If after this callback the Client
  // has a valid access token, the request will continue. Otherwise, it will be aborted with
  // an Exception.
  
  $refreshToken = ... // your implementation of getting the refresh token from the storage
  $client->authenticateByRefreshToken('your-client-id', 'your-client-secret', $refreshToken);
  $accessToken = $client->getAccessToken();
  ... // store $accessToken for future use
});

$orders = $client->getOrders();

The example above assumed your Bol.com integration account uses a refresh token that does not change after use (named 'Method 1' by Bol.com).

If your refresh code changes after each use ('Method 2') then you need to store the new refresh token after refreshing. In this case a refresh token can only be used once. When multiple processes are refreshing simultaneously, there is a risk that due to race conditions a used refresh token is stored last. This means that from then on it's impossible to refresh and the client needs to manually log in again. To prevent this, you need to work with locks, in such a way that it guarantees that only the latest refresh token is stored and used. The example below uses a blocking mutex.

$client = new \Picqer\BolRetailerV8\Client();

$accessToken = ... // your implementation of getting the access token from the storage
$client->setAccessToken($accessToken);

$client->setAccessTokenExpiredCallback(function(\Picqer\BolRetailerV8\Client $client) use ($mutex) {
  // Called at the beginning of a request to the Retailer API when the token was expired.
  
  // Ensure only 1 process can be in the critical section, others are blocked and one is let in when that process leaves
  // the critical section
  $mutex->withLock(function () use ($client) {
    $accessToken = ... // your implementation of getting the latest access token from the storage (it might be refreshed by another process)
    
    if (! $accessToken->isExpired()) {
      // No need to refresh the token, as it was already refreshed by another proces. Make sure the client uses it.
      $client->setAccessToken($accessToken);
      return;
    }
  
    $refreshToken = ... // your implementation of getting the refresh token from the storage
    $newRefreshToken = $client->authenticateByRefreshToken('your-client-id', 'your-client-secret', $refreshToken);
    $accessToken = $client->getAccessToken();
    
    ... // store $accessToken and $newRefreshToken for future use
  }
});

$orders = $client->getOrders();

Copy link
Member

@kleiram kleiram left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ziet er goed uit. Ik heb zelf wel een beetje een vreemd gevoel bij de setAccessTokenExpiredCallback. Ik denk dat ik de flow van bol.com daar niet goed genoeg voor begrijp, maar het voelt als iets wat de client zelf zou moeten kunnen doen. Aan de andere kant denk ik (en correct me if wrong) dat dit er is omdat de refresh tokens ongeldig worden als er een nieuwe refresh token wordt gemaakt.

En klopt het ook dat er nu geen ondersteuning is voor de eerste stap in de auth_code flow? Dus het redirecten naar bol.com voor de authenticatie/authorizatie?

src/BaseClient.php Show resolved Hide resolved
src/BaseClient.php Outdated Show resolved Hide resolved
src/BaseClient.php Outdated Show resolved Hide resolved
src/BaseClient.php Outdated Show resolved Hide resolved
src/BaseClient.php Show resolved Hide resolved
src/JWTToken.php Outdated Show resolved Hide resolved
@robvanaarle
Copy link
Contributor Author

robvanaarle commented Mar 15, 2023

Ziet er goed uit. Ik heb zelf wel een beetje een vreemd gevoel bij de setAccessTokenExpiredCallback. Ik denk dat ik de flow van bol.com daar niet goed genoeg voor begrijp, maar het voelt als iets wat de client zelf zou moeten kunnen doen. Aan de andere kant denk ik (en correct me if wrong) dat dit er is omdat de refresh tokens ongeldig worden als er een nieuwe refresh token wordt gemaakt.

Dat had ik wellicht iets beter kunnen uitleggen in de description van deze PR. Ik heb het wel geprobeerd in de readme, maar dat is denk ik niet duidelijk genoeg? De client kan dit niet zelf doen, omdat de refresh token inderdaad ongeldig kan worden na gebruik. Op het moment dat de access token is verlopen heeft de client de nieuwste refresh token nodig, maar hoe komt deze daar aan? Dit zou via een storage abstractie kunnen, maar ik heb gekozen voor een callback. Ook omdat er nog meer eisen zijn: het opslaan van deze refresh token dient thread safe gebeuren, of te wel binnen een mutex of andere vorm van locking. Voor locking bestaat niks standaards in PHP, dus hiervoor ook een abstractie toevoegen maakt het geheel wel erg complex. Via een callback die het verlopen van de access token afhandeld blijft het wat eenvoudiger en is er wat meer vrijheid in implementatie.

Zie bv deze tijdelijke implementatie (nog niet thread-safe) in Picqer: https://github.com/picqer/picqer/blob/779421fa7e5eb6049c6e58fbffdd7e3ce33bf4fb/Picqer/Webshops/Webshops/BolRetailerV8/Client.php#L99

En klopt het ook dat er nu geen ondersteuning is voor de eerste stap in de auth_code flow? Dus het redirecten naar bol.com voor de authenticatie/authorizatie?

Dat klopt. Dit is een API Client. Bij de eerste stap is de API niet betrokken.

@jobjen02
Copy link

Dit zou een goede oplossing zijn. Het probleem momenteel is dat de token telkens opnieuw wordt aangemaakt en bol.com je hiervoor kan bannen. Het is dus een goede om deze te hergebruiken.

@jobjen02
Copy link

jobjen02 commented Apr 6, 2023

Wanneer wordt hier verder naar gekeken? Met deze update zou ik elke minuut kunnen checken of bestellingen, zodat hij de API token hergebruikt. Momenteel wordt dan telkens een nieuwe aangemaakt, en dat vind bol niet leuk :)

@robvanaarle
Copy link
Contributor Author

Deze aanpassingen zijn onderdeel van een grotere wijziging in Picqer. Momenteel testen we de een deel daarvan (inclusief deze PR) bij enkele klanten en rondden we de rest af. Als dat allemaal goed gaat, dan zal het niet lang meer duren voordat deze PR gemerged wordt.

@robvanaarle robvanaarle marked this pull request as ready for review April 7, 2023 03:57
@robvanaarle robvanaarle merged commit 37305ab into main Apr 7, 2023
@robvanaarle robvanaarle deleted the code-flow-authentication branch April 7, 2023 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
3 participants