API OpenFlyers: Difference between revisions
imported>Gobin |
imported>Gobin |
||
Line 226: | Line 226: | ||
===Signature=== | ===Signature=== | ||
L'en-tête HTTP de signature est structuré de la manière suivante : <code>Signature: keyId="<keyId>",algorithm="<algo>",headers="<signed_headers>",signature="<signature>"</code> | |||
Le ''keyId'' correspond à un identifiant permettant d'identifier ou récupérer la clé utilisée pour vérifier la signature. Pour l'API d'OpenFlyers, sa valeur correspond à l'empreinte SHA-1 du certificat au format PEM à utiliser pour vérifier la signature. | Le champ ''keyId'' correspond à un identifiant permettant d'identifier ou récupérer la clé utilisée pour vérifier la signature. Pour l'API d'OpenFlyers, sa valeur correspond à l'empreinte SHA-1 du certificat au format PEM à utiliser pour vérifier la signature. | ||
L' | L'algorithme ''algo'' correspond à celui utilisé pour générer la signature, exemple : <code>rsa-sha256</code>. | ||
La valeur de '' | La valeur de ''signed_headers'' correspond à la liste des en-têtes inclus dans la signature séparés d'un espace. Exemple : <code>date digest (request-target)</code> | ||
Pour générer la signature, une chaîne de caractères <code>signing string</code> contenant les | Pour générer la signature, une chaîne de caractères appelée <code>signing string</code> contenant les en-têtes au format <code>lowercase_header_name: value</code> séparés par une nouvelle ligne au format LF (<code>\n</code>) est d'abord générée. Exemple avec les en-têtes "date" et "(request-target)" : | ||
<pre>(request-target): post /some/uri\ndate: Tue, 07 Jun 2014 20:51:35 GMT</pre> | <pre>(request-target): post /some/uri\ndate: Tue, 07 Jun 2014 20:51:35 GMT</pre> | ||
Line 241: | Line 241: | ||
;Exemple en php | ;Exemple en php | ||
<php>function generateSignatureHeader(array $headersToSign, string $ | <php>function generateSignatureHeader(array $headersToSign, string $certificateFingerprint, string $privateKey): string | ||
{ | { | ||
// generating the signing string and header list | // generating the signing string and header list | ||
Line 262: | Line 262: | ||
// compiling the header line | // compiling the header line | ||
return "Signature: keyId=\"$ | return "Signature: keyId=\"$certificateFingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\""; | ||
}</php> | }</php> | ||
Les variables ''$ | Les variables ''$certificateFingerprint'' et ''$privateKey'' correspondent respectivement à une empreinte SHA-1 de certificat et à une clé privée, tous deux au format PEM. | ||
La variable ''$headersToSign'' est un | La variable ''$headersToSign'' est un tableau formaté de la manière suivante : | ||
<php>[ | <php>[ | ||
$headerName => $headerValue, | $headerName => $headerValue, | ||
Line 272: | Line 272: | ||
]</php> | ]</php> | ||
'''À noter''' : les en-têtes sont à signer côté client avec le certificat de signature signé par le CA d'OpenFlyers et dédié au client ainsi que la clé privée associée. Les en-têtes de la réponse du serveur quant à elles doivent être vérifiées avec le certificat de signature HTTP du serveur, téléchargeable depuis l'interface de configuration des clients OAuth2. | '''À noter''' : les en-têtes sont à signer côté client avec le certificat de signature signé par le CA d'OpenFlyers et dédié au client ainsi que la clé privée associée. Le certificat de signature dédié au client est téléchargeable depuis l'interface de gestion des clients OAuth2. Les en-têtes de la réponse du serveur quant à elles doivent être vérifiées avec le certificat de signature HTTP du serveur, téléchargeable aussi depuis l'interface de configuration des clients OAuth2. | ||
==Client OAuth2== | ==Client OAuth2== |
Revision as of 12:32, 27 July 2021
Présentation
L'objet de cette page est de décrire :
- l'ancien module de vérification d'un couple identifiant/mot de passe basé sur le script checkIdent.php
- Le nouveau module d'identification basé sur le protocole OAuth2.
L'ancien module checkIdent.php est prévu être désactivé au 31/12/2021.
Ancien module d'identification checkIdent.php
Ce chapitre explique comment vérifier qu'un couple identifiant/mot de passe envoyé, par vos propres scripts, est conforme à la base de données d'OpenFlyers.
Le script retourne une valeur indiquant si la connexion, avec des identifiants données, a réussi et son état. Un cookie OpenFlyers est aussi retourné, permettant de gérer une session utilisateur sur votre site, en utilisant le compte utilisateur OpenFlyers de l'utilisateur connecté.
- Comment ça marche
Si votre plateforme OpenFlyers se situe sur le lien https://openflyers.com/nom-plateforme/, envoyez simplement une requête POST sur le lien https://openflyers.com/nom-plateforme/checkIdent.php avec comme paramètres les variables login et rawPassword.
Attention: Le mot de passe nécessite d'être chiffré en MD5 (cf. la ligne $postData dans le script PHP).
- Valeurs de retour possibles
Le script retourne un chiffre parmi les suivant :
- 0 : OK
- 1 : OK mais plusieurs profils disponibles. OpenFlyers sélectionne automatiquement le meilleur profil.
- 2 : Expiré mais autorisé
- 3 : Expiré mais autorisé, avec un profil expiré
- 4 : Abonnement expiré, refusé
- 5 : Mauvais identifiants, refusé
- 6 : IP ou identifiants bloqués, refusé
- 7 : Aucun identifiant donné, ils sont demandés
- 8 : Authentification réussie mais avec des contrats non signés, bloquant tant qu'il reste des contrats à signer. Pour signer les contrats se connecter sur la plateforme OpenFlyers avec le compte bloqué puis signer les contrats à la connexion.
- 9 : L'abonnement à la plateforme est périmé, le couple identifiant/mot de passe n'est pas vérifié, accès refusé
Nous vous recommandons de considérer un code de retour entre 0 et 2 comme bon et mauvais entre 3 et 8.
Attention: Vous devez filtrer les identifiants de connexion libres (sans droits) puisque pour OpenFlyers, ils correspondent à des accès autorisés !!!
- JavaScript
Si vous utilisez votre propre formulaire d'authentification, utilisez la fonction javascript submit_pwd() située dans \javascript\submitPwd.js .
- Exemple de code PHP
Voici un exemple de code PHP permettant d'envoyer une requête POST : <php>// PHP 5.6 is required // OpenSSL 1.0.1 is required function httpPostRequest($host, $path, $postData) {
$result= ""; $request = "POST $path HTTP/1.1\n". "Host: $host\n". (isset($referer) ? "Referer: $referer\n" : ""). "Content-type: Application/x-www-form-urlencoded\n". "Content-length: ".strlen($postData)."\n". "Connection: close\n\n". $postData."\n"; // Some debug informations:
print("
Request:\n".htmlentities($request)."
");
if ($fp = fsockopen($host, 443, $errno, $errstr, 3)) { // Set cryptology method // @link http://php.net/manual/en/function.stream-socket-enable-crypto.php if (!defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { die('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT IS REQUIRED'); } $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; // Activate encryption while authenticating stream_socket_enable_crypto($fp, true, $cryptoMethod); if (fputs($fp, $request)) { while(! feof($fp)) { $result.= fgets($fp, 128); } // Deactivate encryption once authenticating done stream_socket_enable_crypto($fp, false); fclose($fp); //print($result); return $result; } }
}
$postData = 'login=jbond&rawPassword='.md5('007'); $rawContent = httpPostRequest('openflyers.com','https://openflyers.com/plateform-name/checkIdent.php',$postData);
list($header, $content) = explode("\r\n\r\n", $rawContent, 2); list($byteQty, $realContent, $dummy) = explode("\r\n", $content, 3);
// the answer is in $realContent</php>
OAuth2
OpenFlyers possède une API OAuth2 qu'il est possible d'utiliser.
OAuth2 est un protocole d'autorisation. Ce protocole permet à un utilisateur de donner un accès à certaines de ses données sur une application à une autre application tierce. Les principaux avantages de ce protocole comparé au partage d'identifiant et mot de passe sont les suivants :
- Gestion des droits plus granuleuse.
- Préservation de la confidentialité des mots de passe.
- Possibilité pour l'utilisateur de révoquer les droits sans changer de mot de passe.
- Possibilité pour l'utilisateur de révoquer les droits d'un ou plusieurs clients indépendamment.
- Le changement de mot de passe ne coupe pas l'accès aux données par les clients.
Un exemple d'utilisation typique : une application tierce souhaite utiliser l'API d'un service web pour interagir avec des données de l'utilisateur. L'application tierce effectue une demande d'autorisation auprès du service. L'utilisateur s'authentifie auprès du service et approuve la demande. L'application tierce obtient un jeton et peu maintenant accèder aux données du l'utilisateur via l'API.
Un exemple d'utilisation en tant que solution SSO : quand quelqu'un se connecte à un service avec son compte Google, il donne accès à ses informations de profil Google au service tiers. Le service tiers utilise ces informations de profil comme identification, et utilise l'authentification effectuée par Google pour sa plateforme.
Préparation
Enregistrement du client
Pour utiliser l'API OAuth2, il faut enregistrer un client OAuth2 auprès d'OpenFlyers. Pour ceci, il faut suivre les étapes suivantes :
- Créer un nouveau profil. Ce profil permet de gérer les droits du client OAuth2.
- Choisir un nom explicite, par exemple "Client oauth rapports".
- Sélectionner les droits à assigner à ce profil. Ces droits limitent les données auxquelles le client OAuth2 a accès.
- Sélectionner les droits relatifs à l'enregistrement de clients OAuth2 dans Généralités admin (colonne Associé aux clients OAuth2).
- Sélectionner les rapports accessibles par le profil précédemment créé.
- Créer un nouvel utilisateur à partir du panneau de gestion. Cet utilisateur est virtuel et représente le serveur sur lequel fonctionne le client OAuth2.
- Des identifiant et nom explicites sont recommandés (exemple : "serv1_oauth_client")
- Créer un nouveau client OAuth2 à partir du menu Imports dans le panneau d'administration.
- Choisir un nom pour le client.
- Sélectionner le mécanisme d'autorisation utilisé par le client :
- Pour utiliser OAuth2 comme solution SSO ou accéder à des données de l'utilisateur, choisir Authorization Code. Cette méthode peut être couplée avec la mémorisation de la connexion (Refresh Token).
- Pour utiliser OAuth2 sans utilisateur ou dans un contexte d'automatisme, choisir Client Credentials.
- Saisir l'URI de redirection vers le client.
- Sélectionner l'utilisateur virtuel créé précédemment.
- Générer deux CSR afin d'obtenir deux certificats signés et les saisir :
- Le premier est utilisé pour l'authentification mutuelle avec mTLS.
- Le second est utilisé pour la signature des en-têtes HTTP.
- Un couple ID/passphrase est généré. La passphrase n'est communiquée qu'une seule fois. Elle doit être sauvegardée en lieu sûr et rester confidentielle.
À noter : les certificats du CA d'OpenFlyers et de signature HTTP du serveur sont nécessaires. Ils sont téléchargeables depuis l'interface de configuration des clients OAuth2.
Dans certains cas d'utilisation, il peut être nécessaire d'ajouter le certificat du CA d'OpenFlyers au Trust Store du système. Si cette étape n'est pas réalisée, les certificats peuvent être considérés comme invalides et donc peuvent ne pas être utilisables. Pour ajouter le certificat CA au Trust Store du système,
- Sous Linux, copier le certificat CA d'OpenFlyers dans le dossier
/usr/local/share/ca-certificates
et exécuter la commandesudo update-ca-certificates
- Sous Windows,
- Double-cliquer sur le certificat CA d'OpenFlyers téléchargé depuis l'interface d'enregistrement des clients OAuth2
- Cliquer sur Installer un certificat...
- Choisir l'emplacement de stockage (utilisateur ou ordinateur) et cliquer sur Suivant puis Suivant et enfin Terminer
Génération des certificats
L'API OAuth2 implémente http-signature et l'authentification TLS mutuelle. Ces mécanismes utilisent chacun une paire certificat/clé privée.
Pour obtenir ces certificats, il faut d'abord générer des Certificate Signing Request (CSR).
La procédure est la suivante :
- Se connecter au serveur qui devra utiliser l'API.
- Naviguer dans le répertoire dans lequel les fichiers doivent être créés.
- Utiliser les deux fichiers de configuration sign_cert.conf et auth_cert.conf ci-dessous et les remplir.
- Fichier de configuration OpenSSL - sign_cert.conf
[req] default_bits = 4096 # taille par défaut des nouvelles clés, peut être surchargé dans la commande encrypt_key = no # chiffrer la clé générée distinguished_name = req_distinguished_name # pointe vers la catégorie spécifiée pour le Distinguished Name x509_extensions = v3_req # pointe vers la catégorie spécifiée pour les extensions x509 prompt = no [req_distinguished_name] C = # code à deux chiffres du pays (ex: FR) ST = # région/état (ex: Gironde) L = # ville (ex: Bordeaux) O = # organisation (ex: OpenFlyers) OU = # unité organisationelle (ex: IT) CN = # nom de domaine (ex: openflyers.com) [v3_req] keyUsage = digitalSignature # pour quelles opérations la clé peut-elle être utilisée
- Fichier de configuration OpenSSL - auth_cert.conf
[req] default_bits = 4096 # taille par défaut des nouvelles clés, peut être surchargé dans la commande encrypt_key = no # chiffrer la clé générée distinguished_name = req_distinguished_name # pointe vers la catégorie spécifiée pour le Distinguished Name x509_extensions = v3_req # pointe vers la catégorie spécifiée pour les extensions x509 prompt = no [req_distinguished_name] C = # code à deux chiffres du pays (ex: FR) ST = # région/état (ex: Gironde) L = # ville (ex: Bordeaux) O = # organisation (ex: OpenFlyers) OU = # unité organisationelle (ex: IT) CN = # nom de domaine (ex: openflyers.com) [v3_req] extendedKeyUsage = clientAuth # pour quelles opérations la clé peut-elle être utilisée
Exécuter les commandes suivantes :
- <bash>openssl req -sha256 -newkey rsa -keyout sign.key -out sign_cert.csr.pem -outform PEM -config sign_cert.conf</bash>
- <bash>openssl req -sha256 -newkey rsa -keyout auth.key -out auth_cert.csr.pem -outform PEM -config auth_cert.conf</bash>
Ces commandes prennent chacune un fichier en entrée : la configuration, et génèrent deux fichiers : un Certificate Signing Request, et une clé privée.
Une fois ces CSR obtenus :
- Les renseigner dans les champs prévus à cet effet lors de la création d'un client et télécharger les certificats signés depuis l'interface une fois le client créé.
- Garder la clé privée confidentielle. Une fuite poserait un risque de sécurité. Elle est utilisée conjointement au certificat distribué par l'autorité de certification.
Authentification TLS Mutuelle mTLS
En général dans une communication HTTPS, seul le serveur a l'obligation de fournir un certificat. Il est également possible pour le client de fournir un certificat, ce qu'on appelle Mutual TLS (ou mTLS).
OpenFlyers associe le client OAuth2 à des données contenues dans un certificat TLS pour l'authentifier.
- Client
- envoyer un certificat
Côté client, on peut utiliser le code suivant pour fournir à cURL le certificat, la clé correspondante ainsi que le certificat du CA d'OpenFlyers à utiliser pour la connexion : <php>curl_setopt_array($request, [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, CURLOPT_CAINFO => $caCertificatePath, CURLOPT_SSLCERT => $certificatePath, CURLOPT_SSLKEY => $keyPath
]);</php>
HTTP-Signature
HTTP-signature utilise le principe de la signature numérique pour garantir l'authenticité et l'intégrité du message HTTP.
La signature est générée à l'aide d'une clé privée, et vérifiée à l'aide de la clé publique correspondante, ou d'un certificat contenant cette clé publique.
HTTP-signature utilise deux headers http :
- Signature : contient les metadonnées relatives à la signature et la signature en elle-même.
- Digest : contient un hash du corps du message.
Digest
Le digest est calculé comme ceci : digest = base64encode(sha256(corps du message))
Et le header est structuré comme suivant : Digest: SHA-256=<digest>
- Exemple en php
<php>$digestHeader = 'Digest: SHA-256=' . base64_encode(hash('sha256', $postData, true));</php>
Signature
L'en-tête HTTP de signature est structuré de la manière suivante : Signature: keyId="<keyId>",algorithm="<algo>",headers="<signed_headers>",signature="<signature>"
Le champ keyId correspond à un identifiant permettant d'identifier ou récupérer la clé utilisée pour vérifier la signature. Pour l'API d'OpenFlyers, sa valeur correspond à l'empreinte SHA-1 du certificat au format PEM à utiliser pour vérifier la signature.
L'algorithme algo correspond à celui utilisé pour générer la signature, exemple : rsa-sha256
.
La valeur de signed_headers correspond à la liste des en-têtes inclus dans la signature séparés d'un espace. Exemple : date digest (request-target)
Pour générer la signature, une chaîne de caractères appelée signing string
contenant les en-têtes au format lowercase_header_name: value
séparés par une nouvelle ligne au format LF (\n
) est d'abord générée. Exemple avec les en-têtes "date" et "(request-target)" :
(request-target): post /some/uri\ndate: Tue, 07 Jun 2014 20:51:35 GMT
La signature est ensuite générée comme ceci : base64encode(algo(signing string))
- Exemple en php
<php>function generateSignatureHeader(array $headersToSign, string $certificateFingerprint, string $privateKey): string {
// generating the signing string and header list $headers = ; $signatureString = ; foreach ($headersToSign as $key => $value) { $normalizedHeaderKey = trim(strtolower($key)); $headers .= $normalizedHeaderKey . ' '; $signatureString .= $normalizedHeaderKey . ': ' . trim($value) . "\n"; }
// trim extra whitespace $headers = trim($headers); $signatureString = trim($signatureString);
// signing the signing string $signature = ; openssl_sign($signatureString, $signature, $privateKey, 'RSA-SHA256'); $signature = base64_encode($signature);
// compiling the header line return "Signature: keyId=\"$certificateFingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\"";
}</php> Les variables $certificateFingerprint et $privateKey correspondent respectivement à une empreinte SHA-1 de certificat et à une clé privée, tous deux au format PEM.
La variable $headersToSign est un tableau formaté de la manière suivante : <php>[
$headerName => $headerValue, ...
]</php>
À noter : les en-têtes sont à signer côté client avec le certificat de signature signé par le CA d'OpenFlyers et dédié au client ainsi que la clé privée associée. Le certificat de signature dédié au client est téléchargeable depuis l'interface de gestion des clients OAuth2. Les en-têtes de la réponse du serveur quant à elles doivent être vérifiées avec le certificat de signature HTTP du serveur, téléchargeable aussi depuis l'interface de configuration des clients OAuth2.
Client OAuth2
Une fois le client configuré sur OpenFlyers, il faut un client pour communiquer avec le serveur d'autorisation et l'API.
Plusieurs bibliothèques simplifiant la création d'un client sont disponibles.
Des scripts client basiques écrits en php sont aussi fournis pour les mécanismes authorization_code et client_credentials
Authorization Code
Ce flux OAuth2 se déroule en plusieurs étapes :
- Le client redirige le navigateur de l'utilisateur vers l'URL d'autorisation.
- Le navigateur de l'utilisateur est redirigé vers l'URL fourni durant la demande ou l'enregistrement du client.
- Le client récupère un code d'autorisation via la redirection précédente, et échange ce code contre un jeton d'accès auprès du serveur d'autorisation.
- Le client peut utiliser ce code d'accès :
- Comme preuve d'authentification pour une solution SSO (Single Sign-On).
- Pour accéder à des données sur le serveur distant en utilisant l'API.
+-----------+ +------+ +-----------+ +-------------+ +-----------+ |Utilisateur| |Client| |Navigateur | | Serveur | | Serveur | +-----+-----+ +--+---+ +-----+-----+ |Autorisation | | Ressources| | | | +------+------+ +-----+-----+ | | | | | | | Demande|d'autorisation | | | +------------------------->+------------------------------>| | | | | | | | |Authentification + Formulaire d'autorisation | | |<---------------------------+--------------------------+<------------------------------+ | | | | | | | | Autorisation | | | +----------------------------+------------------------->+------------------------------>| | | | | | | | | Code|d'autorisation | | | |<-------------------------+<------------------------------+ | | | | | | | | Demande de|token | | | +--------------------------+------------------------------>| | | | | | | | | Access (+|Refresh) token | | | |<-------------------------+-------------------------------+ | | | | | | | | | Requête vers API | | | +--------------------------+-------------------------------+------------------------------>| | | | | | | | | | | | | | |<------------------------------+ | | | | Vérification d'autorisation | | | | +------------------------------>| | | | | | | | | Données/Réponse | | | |<-------------------------+-------------------------------+-------------------------------+ | | | | |
Générer les codes pour PKCE
Pour faire fonctionner le client OAuth2, il faut générer deux codes pour l'extension PKCE :
- Un code_verifier échangé pendant la demande de jeton d'accès.
- Un code_challenge dérivé du code_verifier et échangé pendant la demande d'autorisation.
- Générer le code_verifier
<php>function generateCodeVerifier($length = 128): string {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~'; $outputCode = ;
for ($i = 0; $i < $length; $i++) { $index = random_int(0, strlen($characters) - 1); $outputCode .= $characters[$index]; }
return $outputCode;
}</php>
Cette fonction permet de générer un code_verifier avec une longueur donnée. Le code_verifier doit avoir une longueur entre 43 et 128 caractères.
- Générer le code_challenge
<php>function computeCodeChallenge(string $codeVerifier): string {
return strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_');
}</php>
Cette fonction génère le code_challenge à partir du code_verifier fourni.
Demande d'autorisation
Pour initier la demande d'autorisation, rediriger le navigateur de l'utilisateur vers l'URL : GET https://openflyers.com/nom-de-plateforme/oauth/authorize.php
.
Il faut également envoyer les paramètres suivants :
Nom | Type | Description |
---|---|---|
client_id | string | L'identifiant client_id reçu pendant l'enregistrement du client. |
response_type | string | Le type de réponse envoyée par le serveur. Doit utiliser la valeur "code" (sans guillements) pour ce mécanisme. |
redirect_uri | string | L'URL vers lequel l'utilisateur est redirigé après la demande d'autorisation. |
scope | string | Liste des droits demandés par le client, séparés par des espaces. |
state | string | Chaîne de caractère aléatoire utilisée pour éviter les attaques CSRF. Fortement Recommandé |
code_challenge | string | Code nécessaire pour le fonctionnement de l'extension PKCE. |
code_challenge_method | string | Méthode utilisée pour générer le code_challenge. Ici, la valeur est "S256". |
Demande de jeton d'accès
Après avoir répondu à la demande, l'utilisateur est redirigé vers l'URL fourni avec la demande ou l'enregistrement du client. Si la demande est acceptée, un code temporaire : code est fourni, ainsi que le paramètre state fourni pendant la demande avec la même valeur. Si la demande est refusée, un code d'erreur est renvoyé.
- Si le paramètre state a une valeur différente de celle envoyée avec la demande, c'est peut-être une tentative d'attaque et il faut refuser la réponse.
Echanger ce code contre un jeton d'accès via l'URL : POST https://openflyers.com/nom-de-plateforme/oauth/access_token.php
Les paramètres suivants sont également nécessaires :
Nom | Type | Description |
---|---|---|
client_id | string | L'identifiant client_id reçu pendant l'enregistrement du client. |
client_secret | string | La passphrase client_secret reçue pendant l'enregistrement du client. |
code | string | Le code temporaire code reçu dans la réponse à la demande d'autorisation. |
grant_type | string | Le mécanisme d'autorisation utilisé. Ici, sa valeur doit être "authorization_code" (sans guillements) |
redirect_uri | string | L'URL de redirection fourni pendant la demande d'autorisation. |
code_verifier | string | Le code_verifier utilisé pour générer le code_challenge de la demande d'autorisation. |
Si la requête est correcte, un jeton d'accès access_token ainsi qu'une refresh_token sont fournis en réponse dans un objet au format JSON. <javascript>{
"token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU-G7fOBOYzOgioSGNIQKI2A2OxjsNBbOOoaHsqNpsQxWZse3rofExAaGKh2tXbeuz1YAVhdLUGYgq-oKRK4ONFhw2NvcRf3QPxQXZImLWw", "refresh_token": "a59ef39fa9bab9b95cd554f921e7f3080a34c90f23d2b8031d692b5ff2d0993dcc392d9c7f9a43242337ef144c1a5fe1d0174413ade973e1b628ac0bbfc39b23973534"
}</javascript>
Script client : authorization code
Voici un exemple basique d'un client OAuth2 pour le mécanisme authorization_code.
Fichier de configuration config.authcode.json : <javascript>{
"client_id": "", "client_secret": "", "authorize_uri": "https://openflyers.com/nom-de-plateforme/oauth/authorize.php", "token_uri": "https://openflyers.com/nom-de-plateforme/oauth/access_token.php", "resource_uri": "https://openflyers.com/nom-de-plateforme/oauth/resources.php", "auth_cert": "/path/to/client/auth_cert.crt", "auth_key": "/path/to/client/auth.key", "sign_cert": "/path/to/client/sign_cert.crt", "sign_key": "/path/to/client/sign.key", "auth_cacert": "/path/to/ca.crt", "sign_cert_server": "/path/to/server/sign_cert_server.crt"
}</javascript>
Ce fichier de configuration doit se situer au même niveau que le script PHP dans le système de fichiers.
Script PHP : <php><?php $localTokenFile = 'token.json'; // create token file if it does not exist if (!file_exists($localTokenFile))
file_put_contents($localTokenFile, );
$config = json_decode(file_get_contents('config.authcode.json'), true); $localToken = json_decode(file_get_contents($localTokenFile), true); //$config['auth_cert'] = preg_replace("/---client_id---/", $config['client_id'], $config['auth_cert']); //$config['auth_key'] = preg_replace("/---client_id---/", $config['client_id'], $config['auth_key']); //$config['sign_cert'] = preg_replace("/---client_id---/", $config['client_id'], $config['sign_cert']); //$config['sign_key'] = preg_replace("/---client_id---/", $config['client_id'], $config['sign_key']); $GLOBALS['config'] = $config;
// Build the baseURL, accounting for protocol, port, name and endpoint. Used for redirection $protocol = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; $port = ($protocol == 'https://' && $_SERVER['SERVER_PORT'] == 443)
|| ($protocol == 'http://' && $_SERVER['SERVER_PORT'] == 80) ? : ':' . $_SERVER['SERVER_PORT'];
$baseURL = $protocol . $_SERVER['SERVER_NAME'] . $port . $_SERVER['PHP_SELF']; $errorLog = 'error_log.log'; $responseLog = 'response_log.log'; $GLOBALS['baseURL'] = $baseURL;
$replacementListItems = [
1 => "year", 2 => "validityTypeId", 3 => "icao", 4 => "profileId", 7 => "accountingId", 8 => "paymentType", 9 => "startDate", 10 => "endDate", 11 => "occupiedSeat", 12 => "date", 13 => "activityTypeId", 14 => "age", 15 => "resourceId", 16 => "personId", 17 => "accountId", 20 => "rightPlacePersonId", 21 => "month", 22 => "numberMonth", 23 => "oneValidityTypeId",
];
//Session cookies are used to store information necessary for the authorization code flow session_start();
/**
* This function is used to make api calls to the RS * * @param $url * @param $post * @param array $headers * * @return mixed */
function apiRequest($url, $post = null, $token = false, $auth = true, $refresh = false, $headers = array()) {
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true);
$method = 'get';
if ($post) { $method = 'post'; $postData = http_build_query($post); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
$headersToSign['Content-Type'] = 'application/x-www-form-urlencoded'; $headersToSign['digest'] = 'SHA-256=' . base64_encode(hash('sha256', $postData, true)); }
$urlComponents = parse_url($url);
$headersToSign['(request-target)'] = $method . ' ' . $urlComponents['path']; $headersToSign['Host'] = $urlComponents['host']; $headersToSign['Date'] = gmdate('D, j M Y H:i:s T');
// generating the signature header $keyId = str_replace("\n", , file_get_contents($GLOBALS['config']['sign_cert'])); $privateKey = file_get_contents($GLOBALS['config']['sign_key']); $headers['Signature'] = generateSignatureHeader($headersToSign, $keyId, $privateKey);
$headers['Accept'] = 'application/json'; $headers['User-Agent'] = $GLOBALS['baseURL']; unset($headersToSign['(request-target)']); $headers += $headersToSign;
if ($token) { $headers['Authorization'] = $token['token_type'] . ' ' . $token['access_token']; }
// formatting the headers $httpFormattedHeaders = []; foreach ($headers as $key => $value) { $httpFormattedHeaders[] = trim($key) . ': ' . trim($value); }
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpFormattedHeaders); curl_setopt($ch, CURLINFO_HEADER_OUT, true);
// for development environment only //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //curl_setopt($ch, CURLOPT_VERBOSE, true);
// defining TLS client certificates for Mutual TLS curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); curl_setopt($ch, CURLOPT_CAINFO, $GLOBALS['config']['auth_cacert']); curl_setopt($ch, CURLOPT_SSLCERT, $GLOBALS['config']['auth_cert']); curl_setopt($ch, CURLOPT_SSLKEY, $GLOBALS['config']['auth_key']);
$response = curl_exec($ch);
// logging errors and responses errorLog(true, $response, $ch);
curl_close($ch);
// in case an authentication request is executed if ($auth) { // required when a refresh token request is issued // because there is only one header in the response // while there are two for regular auth requests if (!$refresh) { list($firstResponseHeaders, $secondResponseHeaders, $responseBody) = explode("\r\n\r\n", $response, 3); } else { list($secondResponseHeaders, $responseBody) = explode("\r\n\r\n", $response, 2); }
$responseHeadersDigest = ; $responseHeadersSignature = ; // extracting digest and signature from headers $responseHeadersArray = explode("\r\n", $secondResponseHeaders); $responseProtocol = array_shift($responseHeadersArray);
foreach ($responseHeadersArray as $value) { list($responseHeadersKeys, $responseHeadersValue) = explode(": ", $value, 2); $responseHeaders[$responseHeadersKeys] = $responseHeadersValue; }
$responseDigestAlgo = ; $responseDigestKey = ; foreach ($responseHeaders as $key => $value) { if (strtolower($key) === "digest") { $responseHeadersDigest = $value; // stripping SHA algorithm for later comparison list($responseDigestAlgo, $responseDigestKey) = explode("=", $responseHeadersDigest, 2); } else if (strtolower($key) === "signature") $responseHeadersSignature = $value; }
// calculating response digest $responseDigest = base64_encode(hash(strtolower(str_replace("-", "", $responseDigestAlgo)), $responseBody, true));
// checking digest validity if ($responseDigest !== $responseDigestKey) { errorLog(false, false, "Digests are not the same"); die(); }
// extracting variables from signature header $signatureArray = explode(",", $responseHeadersSignature); foreach ($signatureArray as $value) { list($signatureKey, $signatureValue) = explode("=", $value, 2); $signature[$signatureKey] = trim($signatureValue, '"'); }
// generating siging string $signingStringArray = explode(" ", $signature['headers']); $signingString = ; foreach ($signingStringArray as $value) { if ($value === "content-type") $signingString = $signingString . trim($value) . ": " . trim($responseHeaders['Content-Type']) . "\n"; else if ($value === "digest") $signingString = $signingString . trim($value) . ": " . trim($responseHeaders['Digest']) . "\n"; else { errorLog(false, false, "Header not yet defined"); die(); } }
// trimming last '\n' character $signingString = trim($signingString);
// decoding signature $decodedSignature = base64_decode($signature['signature']);
// verifying signature $signatureVerify = openssl_verify($signingString, $decodedSignature, openssl_get_publickey(file_get_contents($GLOBALS['config']['sign_cert_server'])), 'RSA-SHA256');
if (!$signatureVerify) { errorLog(false, false, "Signature is not correct"); while (($err = openssl_error_string())) errorLog(false, false, $err); die(); }
return json_decode($responseBody, true); }
return $response;
}
/**
* This function logs curl responses and errors in text files * * @param mixed $response the response * @param mixed $request the request handle, used to get errors */
function errorLog($curl, $response, $request = false) {
$timestamp = '[' . date('Y-m-d H:i:s') . ']:'; global $errorLog; global $responseLog;
if ($response === false) { if ($curl) file_put_contents($errorLog, $timestamp . curl_error($request) . "\n", FILE_APPEND); else file_put_contents($errorLog, $timestamp . $request . "\n", FILE_APPEND); } else { file_put_contents($responseLog, $timestamp . "\n" . $response . "\n", FILE_APPEND); }
}
/**
* This function generates the full signature header line from a set of headers, a certificate and the linked private key * * @param array $headersToSign the headers to sign, in the $key => $value format * @param string $certificate the certificate linked to the used private key * @param string $privateKey the private key used to sign the headers * * @return string the full signature header line */
function generateSignatureHeader(array $headersToSign, string $certificate, string $privateKey): string {
// generating the signing string and header list $headers = ; $signatureString = ; foreach ($headersToSign as $key => $value) { $normalizedHeaderKey = trim(strtolower($key)); $headers .= $normalizedHeaderKey . ' '; $signatureString .= $normalizedHeaderKey . ': ' . trim($value) . "\n"; } $headers = trim($headers); $signatureString = trim($signatureString);
// signing the signing string $signature = ; openssl_sign($signatureString, $signature, openssl_get_privatekey($privateKey), 'RSA-SHA256'); $signature = base64_encode($signature);
// compiling the header line return "keyId=\"$certificate\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\"";
}
/**
* This function takes a code_verifier and outputs the corresponding code_challenge * * @param string $codeVerifier the generated code_verifier * * @return string the computed code_challenge */
function computeCodeChallenge(string $codeVerifier): string {
return strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_');
}
/**
* This function takes an optional string length and outputs a random code_verifier string * * @param int $length the length of the output code_verifier. Default = 128 * * @return string the code_verifier * @throws Exception */
function generateCodeVerifier($length = 128): string {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~'; $outputCode = ;
for ($i = 0; $i < $length; $i++) { $index = random_int(0, strlen($characters) - 1); $outputCode .= $characters[$index]; }
return $outputCode;
}
/**
* This function calculates the entropy of a given string * * @param string $string the string for which to calculate the entropy * @param string $charset a string with all the usable characters. Default = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~ * * @return float|int */
function computeEntropy(string $string, string $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~') {
$chars = str_split($charset); $probs = [];
foreach ($chars as $char) { $probs[$char] = floatval(substr_count($string, $char)) / strlen($string); }
$sum = 0.0; foreach ($probs as $prob) { $sum += $prob != 0 ? $prob * log($prob, 2) : 0.0; }
return -$sum;
}
/**
* This function calculates the ideal (maximum) entropy for a string of a given length * * @param int $length length of the string. Default = 128 * * @return float|int */
function idealEntropy(int $length = 128) {
$prob = 1.0 / $length;
return -1.0 * $length * $prob * log($prob, 2);
}
/**
* This function is used to initiate the authentication code flow. * * @param string $clientID the client's ID * @param string $redirectURL the URL where to redirect after auth * @param string $authorizeURL the target URL to request authorization * * @throws Exception */
function login(string $clientID, string $redirectURL, string $authorizeURL): void {
global $localTokenFile; file_put_contents($localTokenFile, );
// This unguessable string is used to prevent csrf attacks $_SESSION['state'] = bin2hex(random_bytes(16));
// Generate code_verifier and code_challenge for PKCE $_SESSION['code_verifier'] = generateCodeVerifier(); $_SESSION['code_challenge'] = computeCodeChallenge($_SESSION['code_verifier']);
// required parameters for the redirection, redirect_uri is where the browser should be redirected // when the user grants (or denies) access, scope are the authorizations (rights) requested $params = [ 'response_type' => 'code', 'client_id' => $clientID, 'redirect_uri' => $redirectURL, 'scope' => 'genericreports.readonly reports.readonly', 'state' => $_SESSION['state'], 'code_challenge' => $_SESSION['code_challenge'], 'code_challenge_method' => 'S256' ];
// redirecting the browser to the AS authorization endpoint to obtain the authorization code header('Location: ' . $authorizeURL . '?' . http_build_query($params));
}
/**
* This function deletes the access token from the session * * @param string $baseURL */
function logout(string $baseURL): void {
global $localTokenFile; file_put_contents($localTokenFile, );
header('Location: ' . $baseURL);
}
function displayMenuLoggedIn(): void {
global $replacementListItems;
echo '
<a href="/">Home</a>
'; echo '
Logged In
';
echo '<form id="view_repos" method="post">'; echo '<label for="report_id">Report ID: </label>'; echo '<input type="number" id="report_id" name="report_id">
'; foreach ($replacementListItems as $value) { echo '<label for="' . $value . '">' . $value . ': </label>'; echo '<input type="text" id="' . $value . '" name="' . $value . '">
'; } echo '<input type="hidden" id="action" name="action" value="view">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'view_repos\').submit(); return false;">View Repos</a>
';
echo '</form>';
echo '<form id="logout" method="post">'; echo '<input type="hidden" id="action" name="action" value="logout">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'logout\').submit(); return false;">Log Out</a>
';
echo '</form>';
}
function displayMenuLoggedOut(): void {
echo '
<a href="/">Home</a>
'; echo '
Not logged in
';
echo '<form id="login" method="post">'; echo '<input type="hidden" id="action" name="action" value="login">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'login\').submit(); return false;">Log In</a>
';
echo '</form>';
}
// display client "home" page if (!isset($_POST['action'])) {
if (!empty($localToken['access_token'])) { displayMenuLoggedIn(); } else { displayMenuLoggedOut(); }
}
if (isset($_POST['action'])) {
$replacementList = []; // Building replacement list foreach ($replacementListItems as $key => $value) { $replacementList[$value] = (isset($_POST[$value]) && strlen($_POST[$value]) > 0) ? $_POST[$value] : null; } // Building query array $repoQueryParameters = [ 'client_id' => $config['client_id'], 'report_id' => (isset($_POST['report_id']) && strlen($_POST['report_id']) > 0) ? $_POST['report_id'] : null, 'replacementList' => $replacementList ]; switch ($_POST['action']) { case 'login': login($config['client_id'], $baseURL, $config['authorize_uri']); break; case 'logout': logout($baseURL); break; case 'view': // call to the resource server $resources = apiRequest( $config['resource_uri'], $repoQueryParameters, $localToken, false );
// Separating headers from body list($resourceHeader, $resourceBody) = explode("\r\n\r\n", $resources, 2);
$resourceHeaders = explode("\r\n", $resourceHeader); $firstHeaderLine = array_shift($resourceHeaders);
// Building form for download CSV button echo '<form method="post">'; foreach ($_POST as $key => $value) { if ($key == "action") { $value = "download"; } echo '<input type="hidden" id="' . $key . '" name="' . $key . '" value="' . $value . '">'; } if (isset($_POST['report_id']) && strlen($_POST['report_id']) > 0 && !isset(json_decode($resourceBody, true)['error'])) { echo '<button>Download as CSV</button>'; } echo '</form>';
// Displaying requested content
echo '
'; echo (strpos($firstHeaderLine, "200")) ? json_decode($resourceBody) : $resourceBody; echo '
';
// Automatic refreshing token once access token is expired if (strpos($firstHeaderLine, "401")) { $errorBody = json_decode($resourceBody, true); if ($errorBody['error'] == 'access_denied' && isset($errorBody['hint'])) { if ($errorBody['hint'] == 'Access token could not be verified') { $token = apiRequest( $config['token_uri'], [ 'grant_type' => 'refresh_token', 'refresh_token' => $localToken['refresh_token'], 'client_id' => $config['client_id'], 'client_secret' => empty($config['client_secret']) ? null : $config['client_secret'] ], false, true, true );
// We store the access token in the session so the user is "connected" // Only if the request has been successfully executed if (isset($token['access_token'])) { file_put_contents($localTokenFile, json_encode($token, true));
// Redirecting the user's browser to the "home" page header('Location: ' . $baseURL); } } } } break; case 'download': // call to the resource server $resources = apiRequest( $config['resource_uri'], $repoQueryParameters, $localToken, false );
// Separating headers from body list($resourceHeader, $resourceBody) = explode("\r\n\r\n", $resources, 2); $resourceHeaders = explode("\r\n", $resourceHeader); array_shift($resourceHeaders);
// Dumping headers to insert them in current page foreach ($resourceHeaders as $key => $value) { header($value); } echo json_decode($resourceBody); break; }
}
// Once the browser has been redirected to the AS to ask for the user's authorization, assuming it has been granted, // the browser will get back here with a "code" parameter in the query string if (isset($_GET['code'])) {
// The AS MUST redirect the browser here with the exact same state parameter we sent it before, so we check if it is indeed the same // to detect if the oauth flow has been tampered with if (!isset($_GET['state']) || $_SESSION['state'] != $_GET['state']) { header('Location: ' . $baseURL . '?error=invalid_state'); die(); }
// We communicate directly with the AS to exchange the code received against an access token. // The id/secret pair is send to authenticate the client, the redirect_uri is sent to verify the code's validity $token = apiRequest( $config['token_uri'], [ 'grant_type' => 'authorization_code', 'client_id' => $config['client_id'], 'client_secret' => empty($config['client_secret']) ? null : $config['client_secret'], 'redirect_uri' => $baseURL, 'code' => $_GET['code'], 'code_verifier' => $_SESSION['code_verifier'] ] );
// We store the access token in the session so the user is "connected" // Only if the request has been successfully executed if (isset($token['access_token'])) { file_put_contents($localTokenFile, json_encode($token, true));
// Redirecting the user's browser to the "home" page header('Location: ' . $baseURL); } else { echo $token; } die();
} </php>
- Ce script n'est qu'un exemple basique conçu pour montrer le fonctionnement du mécanisme afin de tester l'implémentation d'un serveur. Il n'a pas été testé extensivement. Il est conseillé d'utiliser une solution qui a fait ses preuves dans un environnement de production.
Client Credentials
Ce mécanisme d'autorisation est adapté pour l'automatisation. Il se déroule comme suivant :
- Le client effectue la demande de jeton d'accès au serveur d'autorisation en fournissant ses identifiants.
- Le serveur authentifie le client avec les identifiants fournis et renvoie un jeton d'accès.
- Le client utilise ce jeton d'accès pour accéder à des données via l'API.
+------+ +-------------+ +-----------+ |Client| | Serveur | | Serveur | +--+---+ |Autorisation | | Ressources| | +------+------+ +-----+-----+ | Authentification | | | + Demande token | | +--------------------------------->| | | | | | Access token | | |<---------------------------------+ | | | | | Requête vers|API | +----------------------------------+------------------------------>| | | | | | | | |<------------------------------+ | | Vérification d'autorisation | | +------------------------------>| | | | | Données /|Réponse | |<---------------------------------+-------------------------------+ | | |
Demande de jeton d'accès
Pour obtenir un jeton d'accès, il faut effectuer la requête suivante : POST https://openflyers.com/nom-de-plateforme/oauth/access_token.php
Les paramètres suivants sont nécessaires :
Nom | Type | Description |
---|---|---|
client_id | string | L'identifiant client_id reçu pendant l'enregistrement du client. |
client_secret | string | La passphrase client_secret reçue pendant l'enregistrement du client. |
grant_type | string | Le mécanisme d'autorisation utilisé. Ici, la valeur doit être "client_credentials" (sans guillements). |
scope | string | Liste des droits demandés par le client, séparé par des espaces. |
Si la requête est correcte, un jeton d'accès access_token est fourni en réponse dans un objet au format JSON. <javascript>{
"token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU-G7fOBOYzOgioSGNIQKI2A2OxjsNBbOOoaHsqNpsQxWZse3rofExAaGKh2tXbeuz1YAVhdLUGYgq-oKRK4ONFhw2NvcRf3QPxQXZImLWw"
}</javascript>
Script client : client credentials
Voici un exemple basique d'un client OAuth2 pour le mécanisme authorization_code.
Fichier de configuration config.clientcred.json : <javascript>{
"client_id": "", "client_secret": "", "token_uri": "https://openflyers.com/nom-de-plateforme/oauth/access_token.php", "resource_uri": "https://openflyers.com/nom-de-plateforme/oauth/resource.php", "auth_cert": "/path/to/client/auth_cert.crt", "auth_key": "/path/to/client/auth.key", "sign_cert": "/path/to/client/sign_cert.crt", "sign_key": "/path/to/client/sign.key", "auth_cacert": "/path/to/ca.crt", "sign_cert_server": "/path/to/server/sign_cert_server.crt"
}</javascript>
Ce fichier de configuration doit si situer au même niveau que le script php dans le système de fichiers.
Script php : <php><?php $localTokenFile = 'token.json'; // create token file if it does not exist if (!file_exists($localTokenFile))
file_put_contents($localTokenFile, );
$config = json_decode(file_get_contents('config.clientcred.json'), true); $localToken = json_decode(file_get_contents($localTokenFile), true); //$config['auth_cert'] = preg_replace("/---client_id---/", $config['client_id'], $config['auth_cert']); //$config['auth_key'] = preg_replace("/---client_id---/", $config['client_id'], $config['auth_key']); //$config['sign_cert'] = preg_replace("/---client_id---/", $config['client_id'], $config['sign_cert']); //$config['sign_key'] = preg_replace("/---client_id---/", $config['client_id'], $config['sign_key']); $GLOBALS['config'] = $config;
// Build the baseURL, accounting for protocol, port, name and endpoint. Used for redirection $protocol = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; $port = ($protocol == 'https://' && $_SERVER['SERVER_PORT'] == 443)
|| ($protocol == 'http://' && $_SERVER['SERVER_PORT'] == 80) ? : ':' . $_SERVER['SERVER_PORT'];
$baseURL = $protocol . $_SERVER['SERVER_NAME'] . $port . $_SERVER['PHP_SELF']; $errorLog = 'error_log.log'; $responseLog = 'response_log.log'; $GLOBALS['baseURL'] = $baseURL;
$replacementListItems = [
1 => "year", 2 => "validityTypeId", 3 => "icao", 4 => "profileId", 7 => "accountingId", 8 => "paymentType", 9 => "startDate", 10 => "endDate", 11 => "occupiedSeat", 12 => "date", 13 => "activityTypeId", 14 => "age", 15 => "resourceId", 16 => "personId", 17 => "accountId", 20 => "rightPlacePersonId", 21 => "month", 22 => "numberMonth", 23 => "oneValidityTypeId",
];
//Session cookies are used to store information necessary for the authorization code flow session_start();
/**
* This function is used to make api calls to the RS * * @param $url * @param $post * @param array $headers * * @return mixed */
function apiRequest($url, $post = null, $token = false, $auth = true, $headers = array()) {
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true);
$method = 'get';
if ($post) { $method = 'post'; $postData = http_build_query($post); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
$headersToSign['Content-Type'] = 'application/x-www-form-urlencoded'; $headersToSign['digest'] = 'SHA-256=' . base64_encode(hash('sha256', $postData, true)); }
$urlComponents = parse_url($url);
$headersToSign['(request-target)'] = $method . ' ' . $urlComponents['path']; $headersToSign['Host'] = $urlComponents['host']; $headersToSign['Date'] = gmdate('D, j M Y H:i:s T');
// generating the signature header $keyId = str_replace("\n", , file_get_contents($GLOBALS['config']['sign_cert'])); $privateKey = file_get_contents($GLOBALS['config']['sign_key']); $headers['Signature'] = generateSignatureHeader($headersToSign, $keyId, $privateKey);
$headers['Accept'] = 'application/json'; $headers['User-Agent'] = $GLOBALS['baseURL']; unset($headersToSign['(request-target)']); $headers += $headersToSign;
if ($token) { $headers['Authorization'] = $token['token_type'] . ' ' . $token['access_token']; }
// formatting the headers $httpFormattedHeaders = []; foreach ($headers as $key => $value) { $httpFormattedHeaders[] = trim($key) . ': ' . trim($value); }
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpFormattedHeaders); curl_setopt($ch, CURLINFO_HEADER_OUT, true);
// for development environment only //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //curl_setopt($ch, CURLOPT_VERBOSE, true);
// defining TLS client certificates for Mutual TLS curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); curl_setopt($ch, CURLOPT_CAINFO, $GLOBALS['config']['auth_cacert']); curl_setopt($ch, CURLOPT_SSLCERT, $GLOBALS['config']['auth_cert']); curl_setopt($ch, CURLOPT_SSLKEY, $GLOBALS['config']['auth_key']);
$response = curl_exec($ch);
// logging errors and responses errorLog(true, $response, $ch);
curl_close($ch);
// in case an authentication request is executed if ($auth) { list($responseHeader, $responseBody) = explode("\r\n\r\n", $response, 2);
$responseHeadersDigest = ; $responseHeadersSignature = ; // extracting digest and signature from headers $responseHeadersArray = explode("\r\n", $responseHeader); $responseProtocol = array_shift($responseHeadersArray);
foreach ($responseHeadersArray as $value) { list($responseHeadersKeys, $responseHeadersValue) = explode(": ", $value, 2); $responseHeaders[$responseHeadersKeys] = $responseHeadersValue; }
$responseDigestAlgo = ; $responseDigestKey = ; foreach ($responseHeaders as $key => $value) { if (strtolower($key) === "digest") { $responseHeadersDigest = $value; // stripping SHA algorithm for later comparison list($responseDigestAlgo, $responseDigestKey) = explode("=", $responseHeadersDigest, 2); } else if (strtolower($key) === "signature") $responseHeadersSignature = $value; }
// calculating response digest $responseDigest = base64_encode(hash(strtolower(str_replace("-", "", $responseDigestAlgo)), $responseBody, true));
// checking digest validity if ($responseDigest !== $responseDigestKey) { errorLog(false, false, "Digests are not the same"); die(); }
// extracting variables from signature header $signatureArray = explode(",", $responseHeadersSignature); foreach ($signatureArray as $value) { list($signatureKey, $signatureValue) = explode("=", $value, 2); $signature[$signatureKey] = trim($signatureValue, '"'); }
// generating siging string $signingStringArray = explode(" ", $signature['headers']); $signingString = ; foreach ($signingStringArray as $value) { if ($value === "content-type") $signingString = $signingString . trim($value) . ": " . trim($responseHeaders['Content-Type']) . "\n"; else if ($value === "digest") $signingString = $signingString . trim($value) . ": " . trim($responseHeaders['Digest']) . "\n"; else { errorLog(false, false, "Header not yet defined"); die(); } }
// trimming last '\n' character $signingString = trim($signingString);
// decoding signature $decodedSignature = base64_decode($signature['signature']);
// verifying signature $signatureVerify = openssl_verify($signingString, $decodedSignature, openssl_get_publickey(file_get_contents($GLOBALS['config']['sign_cert_server'])), 'RSA-SHA256');
if (!$signatureVerify) { errorLog(false, false, "Signature is not correct"); while (($err = openssl_error_string())) errorLog(false, false, $err); die(); }
return json_decode($responseBody, true); }
return $response;
}
/**
* This function logs curl responses and errors in text files * * @param mixed $response the response * @param mixed $request the request handle, used to get errors */
function errorLog($curl, $response, $request = false) {
$timestamp = '[' . date('Y-m-d H:i:s') . ']:'; global $errorLog; global $responseLog;
if ($response === false) { if ($curl) file_put_contents($errorLog, $timestamp . curl_error($request) . "\n", FILE_APPEND); else file_put_contents($errorLog, $timestamp . $request . "\n", FILE_APPEND); } else { file_put_contents($responseLog, $timestamp . "\n" . $response . "\n", FILE_APPEND); }
}
/**
* This function generates the full signature header line from a set of headers, a certificate and the linked private key * * @param array $headersToSign the headers to sign, in the $key => $value format * @param string $certificate the certificate linked to the used private key * @param string $privateKey the private key used to sign the headers * * @return string the full signature header line */
function generateSignatureHeader(array $headersToSign, string $certificate, string $privateKey): string {
// generating the signing string and header list $headers = ; $signatureString = ; foreach ($headersToSign as $key => $value) { $normalizedHeaderKey = trim(strtolower($key)); $headers .= $normalizedHeaderKey . ' '; $signatureString .= $normalizedHeaderKey . ': ' . trim($value) . "\n"; } $headers = trim($headers); $signatureString = trim($signatureString);
// signing the signing string $signature = ; openssl_sign($signatureString, $signature, openssl_get_privatekey($privateKey), 'RSA-SHA256'); $signature = base64_encode($signature);
// compiling the header line return "keyId=\"$certificate\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\"";
}
/**
* This function deletes the access token from the session * * @param string $baseURL */
function logout(string $baseURL): void {
global $localTokenFile; file_put_contents($localTokenFile, );
header('Location: ' . $baseURL);
}
function displayMenuLoggedIn(): void {
global $replacementListItems;
echo '
<a href="/">Home</a>
'; echo '
Logged In
';
echo '<form id="view_repos" method="post">'; echo '<label for="report_id">Report ID: </label>'; echo '<input type="number" id="report_id" name="report_id">
'; foreach ($replacementListItems as $value) { echo '<label for="' . $value . '">' . $value . ': </label>'; echo '<input type="text" id="' . $value . '" name="' . $value . '">
'; } echo '<input type="hidden" id="action" name="action" value="view">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'view_repos\').submit(); return false;">View Repos</a>
';
echo '</form>';
echo '<form id="logout" method="post">'; echo '<input type="hidden" id="action" name="action" value="logout">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'logout\').submit(); return false;">Log Out</a>
';
echo '</form>';
}
function displayMenuLoggedOut(): void {
echo '
<a href="/">Home</a>
'; echo '
Not logged in
';
echo '<form id="login" method="post">'; echo '<input type="hidden" id="action" name="action" value="login">';
echo '
<a href="javascript:{}" onclick="document.getElementById(\'login\').submit(); return false;">Log In</a>
';
echo '</form>';
}
// display client "home" page if (!isset($_POST['action'])) {
if (!empty($localToken['access_token'])) { displayMenuLoggedIn(); } else { displayMenuLoggedOut(); }
}
if (isset($_POST['action'])) {
$replacementList = []; // Building replacement list foreach ($replacementListItems as $key => $value) { $replacementList[$value] = (isset($_POST[$value]) && strlen($_POST[$value]) > 0) ? $_POST[$value] : null; } // Building query array $repoQueryParameters = [ 'client_id' => $config['client_id'], 'report_id' => (isset($_POST['report_id']) && strlen($_POST['report_id']) > 0) ? $_POST['report_id'] : null, 'replacementList' => $replacementList ]; switch ($_POST['action']) { case 'login': $token = apiRequest( $config['token_uri'], [ 'grant_type' => 'client_credentials', 'client_id' => $config['client_id'], 'client_secret' => empty($config['client_secret']) ? null : $config['client_secret'], 'scope' => 'genericreports.readonly reports.readonly' ] ); // We store the access token in the session so the user is "connected" // Only if the request has been successfully executed if (isset($token['access_token'])) { file_put_contents($localTokenFile, json_encode($token, true));
// Redirecting the user's browser to the "home" page header('Location: ' . $baseURL); } else { echo $token; } break; case 'logout': logout($baseURL); break; case 'view': // call to the resource server $resources = apiRequest( $config['resource_uri'], $repoQueryParameters, $localToken, false );
// Separating headers from body list($resourceHeader, $resourceBody) = explode("\r\n\r\n", $resources, 2);
$resourceHeaders = explode("\r\n", $resourceHeader); $firstHeaderLine = array_shift($resourceHeaders);
// Building form for download CSV button echo '<form method="post">'; foreach ($_POST as $key => $value) { if ($key == "action") { $value = "download"; } echo '<input type="hidden" id="' . $key . '" name="' . $key . '" value="' . $value . '">'; } if (isset($_POST['report_id']) && strlen($_POST['report_id']) > 0 && !isset(json_decode($resourceBody, true)['error'])) { echo '<button>Download as CSV</button>'; } echo '</form>';
// Displaying requested content
echo '
'; echo (strpos($firstHeaderLine, "200")) ? json_decode($resourceBody) : $resourceBody; echo '
';
break; case 'download': // call to the resource server $resources = apiRequest( $config['resource_uri'], $repoQueryParameters, $localToken, false );
// Separating headers from body list($resourceHeader, $resourceBody) = explode("\r\n\r\n", $resources, 2); $resourceHeaders = explode("\r\n", $resourceHeader); array_shift($resourceHeaders);
// Dumping headers to insert them in current page foreach ($resourceHeaders as $key => $value) { header($value); } echo json_decode($resourceBody); break; }
}
</php>
- Ce script n'est qu'un exemple basique conçu pour montrer le fonctionnement du mécanisme et tester l'implémentation d'un serveur, il n'a pas été testé extensivement. Il est conseillé d'utiliser une solution qui a fait ses preuves dans un environnement de production.
Refresh Token
Refresh Token (ou jeton de rafraîchissement en français) est un mécanisme d'autorisation particulier. Il ne peut fonctionner en tant que tel, il fonctionne de pair avec Authorization Code. Lorsqu'un jeton d'accès arrive est arrivé en fin de vie, si le client y est autorisé, il peut faire une demande de renouvellement de son jeton d'accès auprès du serveur d'autorisation en présentant son jeton de rafraîchissement. Le serveur d'autorisation vérifie la validité et génère un autre jeton d'accès qu'il transmet au client, sans que l'utilisateur final n'aît à se connecter de nouveau.
Le principe de fonctionnement du rafraîchissement d'un jeton est le suivant :
- Le jeton d'accès du client est arrivé à péremption. Le client effectue une demande de renouvellement en transmettant son Refresh Token au serveur d'autorisation.
- Le serveur d'autorisation vérifie la validité des informations, "consomme" le jeton de rafraîchissement et génère une nouvelle paire de jetons
- Le client reçoit sa nouvelle paire et utilise le nouveau jeton d'accès pour accéder aux ressources
+------+ +-------------+ +-----------+ |Client| | Serveur | | Serveur | +--+---+ |Autorisation | | Ressources| | +------+------+ +-----+-----+ | Authentification | | | + Refresh Token | | +--------------------------------->| | | | | | Access (+ Refresh) token | | |<---------------------------------+ | | | | | Requête vers|API | +----------------------------------+------------------------------>| | | | | | | | |<------------------------------+ | | Vérification d'autorisation | | +------------------------------>| | | | | Données /|Réponse | |<---------------------------------+-------------------------------+ | | |
Demande de renouvellement d'un jeton
Pour obtenir un renouvellement de jeton d'accès, il faut effectuer la requête suivante : POST https://openflyers.com/nom-de-plateforme/oauth/access_token.php
Les paramètres suivants sont nécessaires :
Nom | Type | Description |
---|---|---|
client_id | string | L'identifiant client_id reçu pendant l'enregistrement du client. |
client_secret | string | La passphrase client_secret reçue pendant l'enregistrement du client. |
grant_type | string | Le mécanisme d'autorisation utilisé. Ici, la valeur doit être "refresh_token" (sans guillements). |
scope | string | (OPTIONNEL) Liste des droits demandés par le client, séparé par des espaces. |
Si la requête est correcte, un jeton d'accès access_token est fourni en réponse dans un objet au format JSON. <javascript>{
"token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU-G7fOBOYzOgioSGNIQKI2A2OxjsNBbOOoaHsqNpsQxWZse3rofExAaGKh2tXbeuz1YAVhdLUGYgq-oKRK4ONFhw2NvcRf3QPxQXZImLWw", "refresh_token": "a59ef39fa9bab9b95cd554f921e7f3080a34c90f23d2b8031d692b5ff2d0993dcc392d9c7f9a43242337ef144c1a5fe1d0174413ade973e1b628ac0bbfc39b23973534"
}</javascript>
Liste des scopes disponibles
Un scope sur OAuth2 correspond à un droit d'accès sur une ressource particulière. Chaque scope est unique et indique de manière explicite le privilège qu'il donne. OpenFlyers définit une liste de scopes dédiée aux ressources accessibles par les clients. Ces scopes sont les suivants :
Nom | Description |
---|---|
genericreports.readonly | Accéder aux rapports génériques autorisés pour le client en lecture seule. |
reports.readonly | Accéder aux rapports personnalisés autorisés pour le client en lecture seule (pas encore implémenté). |
Utiliser l'API
Une fois un jeton d'accès obtenu, les données sont accédées en exécutant des requêtes sur l'URL : https://openflyers.com/nom-de-plateforme/oauth/resources.php
Le jeton d'accès doit être renseigné dans le header Authorization avec le format suivant : Authorization: Bearer <access_token>
Plugin d'authentification Joomla
Si vous avez un site Joomla et que vous désirer de permettre aux utilisateurs OpenFlyers de se connecter à votre espace restreint Joomla, vous devriez ajouter ce plugin de manière à avoir une unique base de données de comptes utilisateurs : celle d'OpenFlyers.
Vous n'avez pas besoin de mettre à jour votre base de données Joomla, ce plugin interroge directement OpenFlyers grâce au script PHP CheckIdent.php.