Service accounts
A service account lets an application send authenticated requests to the Webmarketer API.
To send requests to the Webmarketer API with a service account, you have to:
The key attached to a service account must be kept in a secure place
- Generate, in your application, a JWT signed with the service account's private key
- Exchange that JWT for an API access token with the Webmarketer OAuth server
- Send the retrieved access token to the API with your requests
Service account key
When you create a key for a service account from the web interface, it is downloaded to your machine as a JSON file.
The file looks like this:
{
"clientId": <unique identifier>,
"serviceAccountEmail": <service account email>,
"privateKeyId": <key identifier>,
"privateKey": <private key in PEM format>
}
That information is needed to generate a JWT and retrieve an API access token from the Webmarketer OAuth server.
Generating a JWT
A JSON Web Token is made of 3 parts: the header, the payload and the signature. The JWT header and payload are JSON objects.
The JWT header expected by the OAuth server must contain the following properties:
kid, holding the key identifier (privateKeyIdin the key file)alg, which defines the algorithm used to sign the JWT; the only allowed algorithm isRS256typ, holding the valueJWT
{
"kid": <privateKeyId>,
"alg": "RS256",
"typ": "JWT"
}
The JWT payload must contain the following properties:
iss, holding the issuer of the JWT (clientIdin the key file)sub, holding the subject the JWT is issued for (serviceAccountEmail)aud, holding the intended audience of the JWT — the URL of the OAuth server route used to retrieve an API access tokenscope, holding a list of scopes requested for the API access tokenexp, giving the date until which the JWT must be considered valid, as a timestamp
{
"iss": <clientId>,
"sub": <serviceAccountEmail>,
"aud": "https://oauth.webmarketer.io/oidc/token",
"scope": "full_access",
"exp": <some number>
}
Once the JWT header and payload are generated, they have to be encoded in base64url (base64 where the + and / characters are replaced by - and _ respectively, and where the = characters have been removed).
The first part of the JWT is obtained by concatenating the encoded header, a dot . and the encoded payload.
The last part is obtained by signing that first part with RSA SHA-256 and the private key held in the key file (privateKey).
Once the signature is generated, the final JWT is obtained by concatenating the first part, a dot . and the base64url-encoded signature.
The generated JWT is made of 3 parts separated by dots:
eyJraWQiOiJwcml2YXRlS2V5SWQiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbGllbnRJZCIsInN1YiI6InNlcnZpY2VBY2NvdW50RW1haWwiLCJhdWQiOiJodHRwczovL29hdXRoLndlYm1hcmtldGVyLmlvL29pZGMvdG9rZW4iLCJzY29wZSI6ImZ1bGxfYWNjZXNzIiwiZXhwIjoxNjI5MzA0NTY0fQ.OZBSymD_ho5OZjQUHHV_2__ctU9Gk7QxHTW-9LwiafsilR8NYwW0GR56S6zlhw-YpEj28wz75KwsiJI4shO2WYkbc3jtGSTqbc7Mw69clfA4XQ0cjxHVUrVKfVgIiX8bAwocIOSdvJhRJTcpmeN7SVjtKp79kFmfRESBcp3YTv_6QOyuFL-hABDqJYF92x0fzV14b-9AsulIb_JZggrKYEgylLPDkvauL68zI34hcV-lyVXG3A-Al5yEUo3qFzEknG-RHrjD4QmtzwttR_fLMj-1s8dlg8oSJtIruzCy8jp9eIAJjJKmLW8zK0KQEqRz-rg7rQwWMIxw_ESP7HMSaA
Examples
- 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;
}
Getting an API access token
Service accounts negotiate API access tokens with the OAuth server using the jwt-bearer grant type.
Simply send a POST request to the /oidc/token route with the following parameters in the request body:
grant_typemust hold the valueurn:ietf:params:oauth:grant-type:jwt-bearerclient_idmatches theclientIdproperty of the keyassertionmust hold the JWT generated earlier
The request body must be in the application/x-www-form-urlencoded format.
The server returns a response of the form:
{
"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"
}
Examples
- 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);
}
}
}
Sending requests to the API
To send an authenticated request to the API, simply add the Authorization header with the value Bearer <jwt>.