Oauth2 token et accès concurrent

Oauth2 token et accès concurrent - PHP - Programmation

Marsh Posté le 04-06-2024 à 14:00:36    

Bonjour,
 
Voici la situation :  
Je développe une API en PHP (je l'appellerai API Client) afin de simplifier l'appel à une API fournie par un site (qui sera l'API Source). L'API Client s'occupe de réduire le nombre de paramètres nécessaires et gère également la partie Oauth2 de l'API Source.
Le process est assez simple : lors d'un appel à l'API Client, je cherche dans ma base de données si j'ai un token Oauth2 valide (l'API Source fournit un token avec une validité de 599 secondes). Si c'est le cas, j'utilise ce token, sinon j'appelle l'API Source pour obtenir un nouveau token, je supprime le token invalide de ma base, et je stocke le nouveau token avec sa nouvelle date de validité, et je suis à nouveau tranquille pour 599 secondes.
En appelant de manière itérative, tout se passe bien.  
Le problème se pose lorsque j'appelle l'API Client depuis un spreadsheet Google. En effet, j'ai l'impression que les appels sont tellement rapprochés (j'ai fait des tests, lors d'un refresh de la page, j'ai parfois 10 appels dans le même 10ème de seconde) que le premier appel qui arrive n'a pas le temps de supprimer le token et d'en obtenir un nouveau, donc le deuxième appel trouve également un token invalide (s'il a lieu avant la suppression) ou un table vide (s'il a lieu après la suppression), et lance donc l'appel à l'API Source pour obtenir un nouveau token. Dans les cas extrêmes, j'ai eu jusqu'à 10 demandes de renouvellement de token alors que l'objectif de stocker cette donnée dans ma base est de réduire le délai et d'optimiser le nombre d'appels à l'API Source.
 
J'ai trainé un peu hier soir pour chercher une solution, et j'ai quelques pistes :  
- gérer ce comportement avec des sémaphores : pas forcément fan de la solution, surtout que je ne vois pas trop comment gérer les "waits" côté client
- modifier mes requêtes pour locker la table le temps que le premier appel obtienne un token
- ajouter des transactions dans mon code, même objectif que l'idée précédente, c'est à dire empêcher deux accès "proches" de demander un nouveau token au même moment
 
Je pense privilégier la solution 3, est-ce qu'il y a d'autres pistes, est-ce que je rate quelque chose ?
 
Merci

Reply

Marsh Posté le 04-06-2024 à 14:00:36   

Reply

Marsh Posté le 04-06-2024 à 18:22:45    

Je penses que ce que tu veux faire va être compliqué en php / Mysql car y'a un délai induit par chaque requête, ne peux tu pas faire autrement pour le stockage de ton token, une session, du cache en ram genre redis ou un fichier en lecture seul seront peut être plus fiable en terme de parallélisation / accès concurrent.
 
Peut être aussi structurer ton code différemment genre:

Code :
  1. SCRIPT 1 :
  2. Regarder si un Token valide Existe en appelant le SCRIPT 2 qui renvoie le token ou un message d'attente
  3.   Si Oui : Poursuivre avec l'appel à l'API
  4.   Si Non : Attendre quelques Millisecond (temps correspondant grosso merdo a temps qu'il te faut pour créer un token en temps normal) avec usleep
  5. Retour au début
  6. SCRIPT 2 :
  7. Vérifier la présence du token
  8.   Si il ne l'est pas, c'est qu'il est en cours de génération, renvoyer au SCRIPT 1 d'attendre
  9.   Vérifier la validité du token
  10.     Si il est valide, renvoyer le token
  11.     Si il ne l'est pas le créer, renvoyer au SCRIPT 1 d'attendre


Sachant que tu peux aussi t'arranger pour avoir un token toujours valide en mettant le script 2 dans un cron tournant toutes les 9 minutes... :o


Message édité par mechkurt le 04-06-2024 à 18:24:08

---------------
D3
Reply

Marsh Posté le 04-06-2024 à 18:57:42    

Merci pour la réponse. Concernant les propositions :  
- la session ne semble pas utilisable dans le cadre d'une API puisque la connexion n'est pas maintenue ouverte via un navigateur (j'avais trouvé ça sur SO) : https://stackoverflow.com/questions [...] n-rest-api
- le cache en RAM avec Redis, ça semble prometteur et ça permet même de donner une durée de validité à une paire clé/valeur, je vais regarder comment je peux déployer ça sur mon serveur de préprod (hébergé chez OVH, du coup j'ai un doute)
- un fichier en lecture seule ça pourrait être le plus simple, et si le temps de traitement est plus rapide que les requêtes, ça pourrait être efficace.
 
Ce que je fais concrètement aujourd'hui en pseudo code (je pourrai mettre le vrai code si besoin) :  
 

Code :
  1. function getToken()
  2. {
  3. select token from api_token where expire_on > current_timestamp
  4. si vide
  5. {
  6.   $token = ''
  7. }
  8. sinon
  9. {
  10.   $token = token du select
  11. }
  12. si $token = '' // on n'a pas trouvé de token valide
  13. {
  14.   appel API source pour créer token
  15.   $token = valeur renvoyée par l'API source
  16.   delete from api_token
  17.   insert into api_token (token, expires_on)
  18. }
  19. return $token
  20. }


 
La principale différence que je vois c'est que tu proposes de séparer les tests en 2 fonctions, pour que le premier appel qui passe "lock" les autres appels quelques millisecondes avec le usleep ? Je ne vois pas comment différencier le premier appel du suivant. Si le

Code :
  1. select token from api_token where expire_on > current_timestamp

ne retourne rien, comment je peux savoir qu'un autre appel est déjà passé avant moi ?
 
Merci !
 
PS : pour ton edit, chez OVH, un chron c'est une fois par heure maximum. Et autre point, l'API source est gratuite jusqu'à 1000 appels par jour, si je fais un appel toutes les 9 minutes "pour rien", je consomme 160 appels, soit 16% de ma dispo.


Message édité par Tibar le 04-06-2024 à 19:00:59
Reply

Marsh Posté le 04-06-2024 à 19:16:00    

Ou alors, j'ajoute un test après le if token = '' pour vérifier si un fichier "bidon" existe. Si oui, je usleep un petit délai et je rappelle getToken, si il n'existe pas, je touch un fichier bidon, je crée mon token et je rm le fichier bidon ?

 

Sinon je fais un fopen dans un try catch plutôt qu'un touch, et si le fichier ne peut pas être créé, c'est qu'un autre appel à déjà lancé le process.


Message édité par Tibar le 04-06-2024 à 19:23:09
Reply

Marsh Posté le 04-06-2024 à 20:31:22    

Pas sur que sur du mutualisé tu ai accès à du cache mémoire comme REDIS, mais je penses qu'un fichier sera mieux qu'une requête SQL car tu peux vérifier sa présence et le supprimer avant de le recréer donc tes appelles suivants ont juste à vérifier que le fichier existe puis qu'il est valide, et ne créeront donc pas plusieurs token...
Alors qu'en SQL tu peux avoir trois SELECT qui renvoie que le truc existe et n'est plus valide (alors que les requêtes d'UPDATE ou DELETE ne sont pas encore passé) donc trois "instance" qui demandent un nouveau token simultanément.
 
Tente ton truc avec un fichier mais prend bien en compte le usleep qui justement devrait permettre ne pas interroger 10 fois (pour les 10 requêtes simultané au serveur) le même script au même moment.
 
Pour la tache planifié toute les 9 minutes, si ta limite c'est 1000/jour tu est large avec une requête toutes les 9 minutes (24h*60m = 1440m/9 => 160 appel), par sécurité tu peux même faire un appel toutes les 5 minutes et rester en dessous des 1000 requête, à moins que tes appels de token compte en plus des appel API, auquel cas il faudrait voir combien tu compte en faire chaque jour... ^^


---------------
D3
Reply

Marsh Posté le 04-06-2024 à 20:41:40    

Oui, en effet, la solution fichier me semble plus simple et moins contraignante côté BDD, qui nécessite la gestion des transactions et des locks.  
Les trois demandes simultanées, c'est exactement ce qui se passe actuellement.
 
Pour le usleep, j'ai déjà quelques centaines d'appels à l'API qui me génère le token, je vais le fixer au temps maximum * 1.5, et de toutes façons, au pire si le premier qui appelle n'a pas eu de réponse, le fichier "lock" devrait toujours être présent, donc retomber dans le usleep.
 
Et oui, d'après ce que j'ai pu voir du peu de métriques disponibles sur l'API Source, les appels de token comptent bien comme un appel à l'API, c'est pour ça que je souhaite mettre en place toute cette mécanique, sinon je générerai un token à chaque appel.
 
Merci pour les infos, je vois si je peux mettre ça en place rapidement et je ferai un petit retour.

Reply

Marsh Posté le 05-06-2024 à 14:17:04    

Bon, j'ai essayé un peu hier soir mais ça n'est pas concluant pour le moment, je pense qu'il va falloir que je reprenne tout ça à tête reposée et que je découpe plus finement mes tests. J'ai eu des comportements très bizarres après le usleep(3000), mais je pense que c'est lié au fait que j'appelais la méthode getToken depuis getToken. Je ne comprends pas trop, il m'est arrivé de logger 4 créations de token lors de 5 appels simultanés, et le 5è a bien trouvé un token existant :  

Code :
  1. class NomClass
  2. {
  3. private getTokenFromDb()
  4. {
  5.  select token from api_token where mes conditions de validité
  6.  //execution de la requête et affectation du résultat dans la variable $token
  7.  return $token //peut retourner un token valide ou une chaine vide si pas de token valide présent en BDD
  8. }
  9. public getToken()
  10. {
  11.  $lock_file_path = 'chemin vers mon fichier de lock';
  12.  $token = $this->getTokenFromDb();
  13.  if ($token = '') //on n'a pas de token valide en BDD
  14.  {
  15.   if (!file_exists($lock_file_path)) //on est le premier appel à demander un token, création du fichier de lock
  16.   {
  17.    fopen($lock_file_path);
  18.    //appel à l'API pour récupérer un token et sa durée de validité
  19.    $token = APIResult['token'];
  20.    $validity = APIResult['validity'];
  21.    delete from api_token; //pour n'avoir que le dernier token dans la table
  22.    insert into api_token(token, expires_on) values ('$token', current_date + $validity);
  23.    rm($lock_file_path);
  24.   }
  25.   else //un autre appel est déjà en train de générer un nouveau token puisque le fichier est là
  26.   {
  27.    usleep(3000); //on attend 3 secondes
  28.    $this->getToken(); //et on recommence l'appel
  29.   }
  30.  }
  31.  return($token);
  32. }
  33. }


 
J'ai également eu des fichiers lock impossible à supprimer sans couper Wamp server, bref, pas très rassurant.

Reply

Marsh Posté le 06-06-2024 à 03:02:52    

Bordel ! Ligne 29 de mon code

Code :
  1. $this->getToken();


 
à remplacer par

Code :
  1. $token = $this->getToken();


 
Ca fonctionne tout de suite beaucoup mieux. Le spreadsheet génère quand même 2 appels en 2.4 millièmes de seconde :  
Appel 1 : 2024-06-06 03:13:21.338520
Appel 2 : 2024-06-06 03:13:21.340920
 
et dans ce cas de figure, la création du fichier ne semble pas terminée, du coup on a 2 demandes au lieu d'une.


Message édité par Tibar le 06-06-2024 à 03:25:25
Reply

Marsh Posté le 06-06-2024 à 07:51:29    

usleep(3000) ce n'est pas 3 secondes (utilise sleep si tu veux attendre en seconde).

Citation :

Une microseconde est un millionième de seconde.


Tu devrais essayer de mettre au début de ton script un usleep(random_int(1, 999999)) pour que tes appels simultané soit suffisamment désynchronisé pour que le premier script (celui ayant le usleep(random()) le plus petit) ai le temps de créer avant que les autres vérifie la présence...
 
Par contre pourquoi tu gardes ton token en base de donnée, puisque tu manipules déjà un fichier, écrit ton token dedans non ?


Message édité par mechkurt le 06-06-2024 à 16:22:31

---------------
D3
Reply

Marsh Posté le 06-06-2024 à 14:55:48    

Ah, mais j'avais essayé le rand au début de la procédure, sauf que je limitais entre 100 et 500, mais forcément, en microsecondes ça ne suffisait pas. Je vais augmenter ces valeurs, ça devrait le faire et ça m'apprendra à mieux lire la doc également, et à ne pas toujours croire le premier commentaire sur S/0 : https://stackoverflow.com/questions [...] sh-example
 
Je garde le token en base de données parce que c'est la première idée qui m'est venue quand j'ai voulu réduire le nombre d'appels, mais en effet, maintenant qu'un fichier est créé, ça serait encore plus rapide de passer par une lecture de fichier pour récupérer le token.
 
Merci pour tes réponses, ça m'a bien débloqué, je tenterai ce soir avec un usleep random un peu plus important, ça devrait encore diminuer le nombre d'appel.
 

Reply

Marsh Posté le 06-06-2024 à 14:55:48   

Reply

Marsh Posté le 13-06-2024 à 14:07:43    

Question bête : si tu as un moyen d'identifier que les x requêtes qui arrivent concerne le même user (ex : via sa clé d'API), pourquoi tu mets pas dans une file t'attente du user concerné les requêtes à exécuter pour les passer séquentiellement. Ainsi, la première va demander l'authentification puis les autres vont être exécutées sans demander l'auth puisque le token valide est dispo les unes à la suite des autres.


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 15-10-2024 à 00:29:33    

Bonjour à tous,
 
De retour sur ce topic... Tout fonctionnait parfaitement jusqu'à la semaine dernière. Les utilisateurs ont commencé à me signaler beaucoup d'erreurs. J'ai fait quelques tests, depuis mon poste en local avec Wamp, tout fonctionne bien, mais lorsque je déploie mon code de test chez OVH, j'ai un timeout.
Du coup, pas évident de débugger.
J'ai testé mon code sur reqbin, tout se passe bien également (j'ai bien la réponse que j'attends).
Voici mon code :  
 

Code :
  1. <?php
  2. $client_id = "dibHs0moqDs80KGLTkiduMuNdwTlvaXb";
  3. $client_secret = "mon_code_secret";
  4. $base_url = "https://api.digikey.com/v1/oauth2/token";
  5. $ch = curl_init();
  6. curl_setopt($ch, CURLOPT_URL, $base_url);
  7. curl_setopt($ch, CURLOPT_POST, TRUE);
  8. curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  9. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
  10. curl_setopt($ch, CURLOPT_POSTFIELDS, 'client_id=' . $client_id .'&client_secret=' . $client_secret . '&grant_type=client_credentials');
  11. $data = curl_exec($ch);
  12. var_dump($data);
  13. ?>


 
J'ai simplifié au maximum, c'est ce code qui fonctionne parfaitement avec Wamp en local, mais qui tombe en timeout lorsqu'il est déployé chez OVH.
Du coup je soupçonne 2 problèmes potentiels :  
1 - OVH ne permet plus de faire des appels CUrl
2 - je suis bloqué au niveau du fournisseur de l'API, qui bloque les appels depuis OVH (ce que je soupçonne, puisque reqbin et local fonctionnent)
 
Dans les deux cas, je ne sais pas trop par où commencer.  
 
Est-ce que quelqu'un a un site chez OVH pour tester ce bout de code ?
 
Normalement, ça doit retourner un Json de ce type :  
 

Code :
  1. {"ErrorResponseVersion":"3.0.0.0","StatusCode": 401,"ErrorMessage":"invalid_client","ErrorDetails":"Client credentials are invalid","RequestId":"8814a525-1bed-4181-8ab4-821bdc1b51db","ValidationErrors":[]}


 
Si besoin, je peux fournir mon client_secret en mp pour récupérer le vrai retour.
 
Merci !

Reply

Marsh Posté le 15-10-2024 à 07:58:34    

Je penses que le serveur de l'API filtre les IPs d'OVH, sans doute à a cause d'abus (probablement pas que les tiens si ils filtrent toutes les IPs ^^).
En ajoutant ce code de debuggage:

Code :
  1. curl_setopt($ch, CURLOPT_VERBOSE, true);
  2. $streamVerboseHandle = fopen('php://temp', 'w+');
  3. curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
  4.     $data = curl_exec($ch);
  5.     var_dump($data);
  6. if ($result === FALSE) {
  7. printf("cUrl error (#%d): %s<br>\n",
  8.   curl_errno($curlHandle),
  9.   htmlspecialchars(curl_error($curlHandle)))
  10.   ;
  11. }
  12. rewind($streamVerboseHandle);
  13. $verboseLog = stream_get_contents($streamVerboseHandle);
  14. echo "cUrl verbose information:\n",
  15.  "<pre>", htmlspecialchars($verboseLog), "</pre>\n";


https://stackoverflow.com/questions [...] gging-curl
 
j'ai comme retour :

Code :
  1. bool(false) cUrl verbose information:
  2. *   Trying 204.221.76.129...
  3. * TCP_NODELAY set
  4. * connect to 204.221.76.129 port 443 failed: Connection timed out
  5. * Failed to connect to api.digikey.com port 443: Connection timed out
  6. * Closing connection 0


---------------
D3
Reply

Marsh Posté le 15-10-2024 à 11:12:02    

Salut,

 

Merci pour les tests effectués ! Je vais fournir ça en plus à l'équipe support de l'API. Ils mettent à disposition une API gratuite avec 1000 appels par jour par client_id, pas par IP, mais j'imagine qu'ils doivent se faire interroger énormément depuis chaque gros hébergeur.
Est-il possible qu'OVH bloque les appels sortants plutôt que Digikey les appels entrants ? Je pense que l'erreur serait différente, mais le support de Digikey semble dire qu'ils n'ont pas de règles de filtrage et qu'ils ne voient pas mes appels.

 

Bonne journée.

Message cité 1 fois
Message édité par Tibar le 15-10-2024 à 11:21:28
Reply

Marsh Posté le 15-10-2024 à 13:08:31    

Tibar a écrit :

Est-il possible qu'OVH bloque les appels sortants plutôt que Digikey les appels entrants ?


Tout est possible, mais je ne vois pas bien pourquoi OVH filtrerait le port 443 en sortie d'une de ses IP mutualisé...
 
Pour moi ça m'évoque plus un serveur distant se méfiant des mutualisé OVH (suffit que y'ai un fail2ban qui tourne de leur coté et que l'IP de ton serveur soit partagé avec celle d'un pirate :o ).
Regarde : https://community.ovh.com/t/PHP-Cur [...] -GET/61047


---------------
D3
Reply

Marsh Posté le 15-10-2024 à 17:04:34    

Merci pour la réponse.  
 
Le support de Digikey me demande l'IP sortante qui génère les appels pour contrôler leurs logs, je vais leur fournir, ça semble compliqué pour pas grand chose cette histoire.  
J'espère vraiment que ça n'est pas un blocage complet de la part de Digikey des IPs d'OVH sinon ça risque de devenir compliqué, et si c'est le cas, je pense que je risque de rencontrer le même type de problème avec d'autres hébergeurs, à moins de prendre un système dédié plutôt qu'un mutualisé, je vais regarder pour les tarifs.

Reply

Marsh Posté le 15-10-2024 à 18:24:12    

Bonne chance pour trouver l'adresse IP qu'utilise ton serveur pour l'appel...
Tu peux essayer ce genre de code qui fait appel à une API tierce :

Code :
  1. <?php
  2.       // create a new cURL resource
  3.       $ch = curl_init ();
  4.       // set URL and other appropriate options
  5.       curl_setopt ($ch, CURLOPT_URL, "http://ipecho.net/plain" );
  6.       curl_setopt ($ch, CURLOPT_HEADER, 0);
  7.       curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
  8.       // grab URL and pass it to the browser
  9.       $ip = curl_exec ($ch);
  10.       echo "The public ip for this server is: $ip";
  11.       // close cURL resource, and free up system resources
  12.       curl_close ($ch);
  13.     ?>


https://stackoverflow.com/questions [...] 3#47364733
 
Dans tous les cas rien ne te garantira qu'un dédié chez OVH (ou un autre hébergeur) ne soit pas blacklisté lui aussi donc essaye déjà de "debugger" le problème avec Digikey.
Après OVH n'a pas très bonne réputation au niveau de ses mutualisé, je supposes qu'un hébergeur "plus cher" te posera moins de problème à ce niveau... :o


---------------
D3
Reply

Marsh Posté le 15-10-2024 à 19:05:15    

mechkurt a écrit :

Bonne chance pour trouver l'adresse IP qu'utilise ton serveur pour l'appel...
Tu peux essayer ce genre de code qui fait appel à une API tierce :

Code :
  1. <?php
  2. // create a new cURL resource
  3. $ch = curl_init ();
  4. // set URL and other appropriate options
  5. curl_setopt ($ch, CURLOPT_URL, "http://ipecho.net/plain" );
  6. curl_setopt ($ch, CURLOPT_HEADER, 0);
  7. curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
  8. // grab URL and pass it to the browser
  9. $ip = curl_exec ($ch);
  10. echo "The public ip for this server is: $ip";
  11. // close cURL resource, and free up system resources
  12. curl_close ($ch);
  13. ?>


https://stackoverflow.com/questions [...] 3#47364733

 

Dans tous les cas rien ne te garantira qu'un dédié chez OVH (ou un autre hébergeur) ne soit pas blacklisté lui aussi donc essaye déjà de "debugger" le problème avec Digikey.
Après OVH n'a pas très bonne réputation au niveau de ses mutualisé, je supposes qu'un hébergeur "plus cher" te posera moins de problème à ce niveau... :o

 

Génial, merci pour le code. Digikey me confirme qu'ils ont plus d'un million d'appels par jour de la part d'OVH, mais ils ne parviennent pas à isoler les miens pour le moment, je pensais bêtement qu'avec le client_id ça serait assez rapide...
Je vais essayer ton code, je pensais faire un truc comme ça aussi, mais je ne connaissais pas ipecho, merci beaucoup !

Reply

Marsh Posté le 16-10-2024 à 02:18:51    

Bonjour,
 
Voilà, j'ai déployé ton bout de code sur mon serveur, j'ai bien récupéré une adresse IP, et j'en ai profité pour mettre un user agent que le support de Digikey me demande pour filtrer leurs logs.
Reste à voir ce qu'ils vont pouvoir trouver avec cette IP.
 
Merci pour l'aide, j'espère revenir avec des bonnes nouvelles rapidement.
 
Edit : et du coup, maintenant que j'ai l'adresse IP, j'ai pu voir qu'elle était disponible sur le support d'OVH : https://help.ovhcloud.com/csm/fr-we [...] =KB0052378
Il faut juste connaitre son cluster.


Message édité par Tibar le 16-10-2024 à 02:25:17
Reply

Marsh Posté le 16-10-2024 à 16:56:00    

Tibar a écrit :

Digikey me confirme qu'ils ont plus d'un million d'appels par jour de la part d'OVH, mais ils ne parviennent pas à isoler les miens pour le moment, je pensais bêtement qu'avec le client_id ça serait assez rapide...

J'y connais rien :o , mais si ils bloquent toute la plage IP au niveau pare-feu ils ne doivent même pas récupérer les requêtes qui contiennent le client_id je pense.


---------------
Ne laissez pas mourir vos sujets en cours de route!
Reply

Marsh Posté le 17-10-2024 à 01:49:10    

rat de combat a écrit :

J'y connais rien :o , mais si ils bloquent toute la plage IP au niveau pare-feu ils ne doivent même pas récupérer les requêtes qui contiennent le client_id je pense.

 

Oui, c'est ce que je me dis aussi, mais comme ils voient d'après eux 1 million d'appels d'OVH par jour, je me dis qu'ils doivent savoir gérer. On verra bien, pour le moment pas de nouvelles du support.

Reply

Marsh Posté le    

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed