Comptes de service
Un compte de service permet à une application d'envoyer des requêtes authentifiées à l'API Webmarketer.
Pour effectuer des requêtes à l'API Webmarketer avec un compte de service il faut :
- Créer un compte de service et une clé pour ce compte depuis l'interface webClé privée
La clé associée à un compte de service doit être conservée de façon sécurisée
- Générer dans votre application un JWT signé avec la clé privée du compte de service
- Échanger ce JWT contre un jeton d'accès à l'API auprès du serveur oauth de Webmarketer
- Envoyer le jeton d'accès récupéré à l'API lors des requêtes
Clé de compte de service
Lors de la création d'une clé pour un compte de service depuis l'interface web, celle-ci est téléchargée sur votre machine sous la forme d'un fichier JSON.
Le contenu du fichier est de la forme :
{
"clientId": <identifiant unique>,
"serviceAccountEmail": <email du compte de service>,
"privateKeyId": <identifiant de la clé>,
"privateKey": <clé privée au format PEM>
}
Ces informations sont nécessaires pour générer un JWT afin de récupérer un jeton d'accès à l'API auprès du serveur oauth de Webmarketer.
Générer un JWT
Un Json Web Token est composé de 3 parties : l'entête, le corps et la signature. L'entête et le corps du JWT sont des objets JSON.
L'entête du JWT attendu par le serveur oauth doit contenir les propriétés suivantes :
kid
contenant l'indentifiant de la clé (privateKeyId
dans le fichier de clé)alg
qui définit l'algorithme utilisé pour signer le JWT, le seul algorithme autorisé estRS256
typ
contenant la valeurJWT
{
"kid": <privateKeyId>,
"alg": "RS256",
"typ": "JWT"
}
Le corps du JWT doit contenir les propriétés suivantes :
iss
contenant l'émetteur du JWT (clientId
dans le fichier clé)sub
contenant le sujet pour lequel le JWT est émis (serviceAccountEmail
)aud
contenant l'audience de destination du JWT, c'est l'url de la route du serveur oauth permettant de récupérer un jeton d'accès à l'APIscope
contenant une liste de scopes demandés pour le jeton d'accès à l'APIexp
indiquant la date jusqu'à laquelle le JWT doit être considéré comme valide, sous la forme d'un timestamp
{
"iss": <clientId>,
"sub": <serviceAccountEmail>,
"aud": "https://oauth.webmarketer.io/oidc/token",
"scope": "full_access",
"exp": <some number>
}
Une fois l'entête est le corps du JWT générés, il faut les encodés en base64url
(base64
où les caractères +
et /
sont remplacés par les caractères -
et _
respectivements et où les caractères =
ont été supprimés).
La première partie du JWT est obtenue par la concaténation de l'entête encodé, d'un point .
et du corps encodé.
La dernière est partie est obtenu en signant la première partie à l'aide de RSA SHA-256
et de la clé privée contenue dans le fichier de clé (privateKey
).
Une fois la signature générée, le JWT final est obtenu en concaténant la première partie, un point .
et la signature encodée en base64url.
Le JWT généré est constitué de 3 parties séparées par des points :
eyJraWQiOiJwcml2YXRlS2V5SWQiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbGllbnRJZCIsInN1YiI6InNlcnZpY2VBY2NvdW50RW1haWwiLCJhdWQiOiJodHRwczovL29hdXRoLndlYm1hcmtldGVyLmlvL29pZGMvdG9rZW4iLCJzY29wZSI6ImZ1bGxfYWNjZXNzIiwiZXhwIjoxNjI5MzA0NTY0fQ.OZBSymD_ho5OZjQUHHV_2__ctU9Gk7QxHTW-9LwiafsilR8NYwW0GR56S6zlhw-YpEj28wz75KwsiJI4shO2WYkbc3jtGSTqbc7Mw69clfA4XQ0cjxHVUrVKfVgIiX8bAwocIOSdvJhRJTcpmeN7SVjtKp79kFmfRESBcp3YTv_6QOyuFL-hABDqJYF92x0fzV14b-9AsulIb_JZggrKYEgylLPDkvauL68zI34hcV-lyVXG3A-Al5yEUo3qFzEknG-RHrjD4QmtzwttR_fLMj-1s8dlg8oSJtIruzCy8jp9eIAJjJKmLW8zK0KQEqRz-rg7rQwWMIxw_ESP7HMSaA
Exemples
- Node.js
- Php
import { sign } from "crypto";
import { readFile } from "fs/promises";
function base64urlEncode(buffer: Buffer): string {
return buffer.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
async function getKey(keyFilePath: string) {
// retrieve the key
const jsonEncodedKey = await readFile(keyFilePath, "utf-8");
const key = JSON.parse(jsonEncodedKey);
return key;
}
function generateJwt(key: Record<string, any>): string {
// generate the header
const header = {
kid: key.privateKeyId,
alg: "RS256",
typ: "JWT",
};
// generate the payload
const payload = {
iss: key.clientId,
sub: key.serviceAccountEmail,
aud: "https://oauth.webmarketer.io/oidc/token",
scope: "full_access",
exp: Date.now() / 1000 + 300, // 5 minutes
};
// first part of the jwt
let jwt = base64urlEncode(Buffer.from(JSON.stringify(header)))
+ "."
+ base64urlEncode(Buffer.from(JSON.stringify(payload)));
// generate the signature
const signature = sign("RSA-SHA256", Buffer.from(jwt), key.privateKey);
// complete jwt
jwt = jwt
+ "."
+ base64urlEncode(signature);
return jwt;
}
<?php
function base64url_encode(string $str): string
{
return rtrim(strtr(base64_encode($str), "+/", "-_"), "=");
}
function get_key(string $key_file_path)
{
// retrieve the key
$json_encoded_key = file_get_contents($key_file_path);
$key = json_decode($json_encoded_key);
return $key;
}
function generate_jwt($key): string
{
// generate the header
$header = new stdClass();
$header->alg = "RS256";
$header->typ = "JWT";
$header->kid = $key->privateKeyId;
// generate the payload
$payload = new stdClass();
$payload->iss = $key->clientId;
$payload->sub = $key->serviceAccountEmail;
$payload->aud = "https://oauth.webmarketer.io/oidc/token";
$payload->scope = "full_access";
// 5 minutes is enough to negociate an access token with the oauth server
$payload->exp = time() + 60 * 5;
// first part of the jwt
$jwt = base64url_encode(json_encode($header)) . "." . base64url_encode(json_encode($payload));
// generate the signature
$signature = "";
if (function_exists("openssl_sign") && in_array("RSA-SHA256", openssl_get_md_methods(true))) {
openssl_sign($jwt, $signature, $key->privateKey, "RSA-SHA256");
} else {
throw new Exception("Missing crypto function openssl_sign() or signing alg RSA-SHA256");
}
// complete jwt
$jwt .= "." . base64url_encode($signature);
return $jwt;
}
Obtenir un jeton d'accès à l'API
Les comptes de service négocient les jetons d'accès à l'API auprès du serveur oauth en utilisant le grant type jwt-bearer
.
Il suffit d'envoyer une requête POST
à la route /oidc/token
avec les paramètres suivants dans le body de la requête :
grant_type
doit avoir la valeururn:ietf:params:oauth:grant-type:jwt-bearer
client_id
correspond à la propriétéclientId
de la cléassertion
doit contenir le JWT généré précédemment
Le body de la requête doit être au format application/x-www-form-urlencoded
.
Le serveur renvoie une réponse de la forme :
{
"access_token": "eyJraWQiOiJwcml2YXRlS2V5SWQiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ1bmlxdWVJZCIsInN1YiI6ImFjY291bnRJZCIsImlhdCI6MTYyOTM4ODg5MCwiZXhwIjoxNjI5MzkyNDkwLCJzY29wZSI6ImZ1bGxfYWNjZXNzIiwiaXNzIjoiaHR0cHM6Ly9vYXV0aC53ZWJtYXJrZXRlci5pbyIsImF1ZCI6ImNsaWVudElkIn0.b1BZJdE-5OKNvRKdM8uEPWVwbDHoxu74p_A6NK1KuQW2hQmKcKDLUNBRqTDpw156P9ll9DORxbBdRKrY988WdvQNMs_ehSgIHdfM-W02EHDxgSXIpyNgj5th76XibP-6Glhvhbo24-ZOFdK7V1EBVYrxcLTviunqw42JrBf59W2OsvOYIuEzDOY3jH8oQ-s20PKoCqq_g5HBcjH_9-eoFLJnwgaiTwtSbuayJiGUJjkjmki1dp_2YRORXFY0VKzsCrd6b4URgYTt6M9o-61WCsmgYiqD--1hZ55hfg4WphSIhlJgSvPl4tqn0_wITPDcgLNRHaT1MDD1m42py5_rDA",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "full_access"
}
Exemples
- cURL
- Node.js
- Php
curl --request POST \
--url https://oauth.webmarketer.io/oidc/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data client_id=<clientId> \
--data assertion=<JWT>
import { request } from "https";
import { stringify } from "querystring";
function requestAccessToken(clientId: string, jwt: string): Promise<{
access_token: string;
expires_in: number;
token_type: string;
scope: string;
}> {
return new Promise((resolve, reject) => {
// construct the body of the request
const body = stringify({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
client_id: clientId,
assertion: jwt,
});
// set request content type and method
const options = {
headers: {
"content-type": "application/x-www-form-urlencoded"
},
method: "POST",
};
request(
"https://oauth.webmarketer.io/oidc/token",
options,
(response) => {
if (response.statusCode !== 200) {
reject(new Error("Invalid response status code : " + response.statusCode));
// consume response data to free up memory
response.resume();
return;
}
let rawData = "";
response.on("error", reject)
.on("data", (chunk) => {
rawData += chunk.toString("utf-8");
})
.on("end", () => {
try {
resolve(JSON.parse(rawData));
} catch (error) {
reject(error);
}
});
}
)
.on("error", reject)
// send request with body
.end(body);
});
}
<?php
function request_access_token(string $client_id, string $jwt)
{
// construct the request body
$body = http_build_query([
"grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer",
"client_id" => $client_id,
"assertion" => $jwt,
]);
// negociate the access token
try {
$ch = curl_init("https://oauth.webmarketer.io/oidc/token");
if (!$ch) {
throw new Exception("Unable to intialize curl session");
}
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response_json = curl_exec($ch);
if (curl_errno($ch)) {
throw new Error(curl_error($ch));
}
$status_code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status_code != 200) {
throw new Error("Status code " . $status_code);
}
$response_data = json_decode($response_json);
return $response_data;
} finally {
if ($ch) {
curl_close($ch);
}
}
}
Envoyer des requêtes à l'API
Pour envoyer une requête authentifiée à l'api, il suffit d'ajouter l'entête Authorization
avec la valeur Bearer <jwt>
.