Вводные
Нужно протестировать работоспособность API в которых:
- Используется подпись в теле запроса, пример Mango office API (документация)
- Используется Oauth аутентификация с RS256, пример Google API используя сервисного пользователя (документация)
- Используется Oauth аутентификация, пример Google API используя доступ обычного пользователя (документация)
- Используется JWT токен HS256, пример Zoom (документация)
- Тестирую с помощью Postman
Описание варианта с подписью
Выдержка из документации: "Данные, которыми обмениваются системы, как правило, будут передаваться в теле POST-запроса. В этом случае в тело запроса включается обязательные параметры json, vpbx_api_key и sign.
Параметр vpbx_api_key заполняется уникальным кодом продукта ВАТС, полученным в Личном кабинете MANGO OFFICE при подключении API.
Значение sign рассчитывается следующим образом: sign = sha256(vpbx_api_key + json + vpbx_api_salt).
Подписываются все запросы — как от внешней системы, так и от API ВАТС."
Решение варианта с подписью
Можно собрать тело запроса вручную, рассчитав хэш SHA256 используя онлайн сервис или написав обработку в 1С, но для тестирования набора запросов с разными данными это неудобно.
В postman поддерживаются переменные (документация) и обработчики перед и после отправки запроса (документация).
Обработчики пишутся на JS, ими можно установить переменную, нельзя переписать тело запроса. Поэтому для решения задачи помещаем переменную {{content_body}} в тело запроса
и реализуем её расчет для Pre-request script
sign = CryptoJS.SHA256(
pm.variables.get("vpbx_api_key") +
pm.variables.get("json") +
pm.variables.get("vpbx_api_salt")
).toString();
pm.variables.set(
"content_body",
"vpbx_api_key=" + pm.variables.get("vpbx_api_key") + "&" +
"sign=" + sign + "&" +
"json=" + encodeURI(pm.variables.get("json"))
);
Расчет подписи используется для всей коллекции при этом данные определяются на уровне запроса. Для решения задачи код нужно установить в коллекцию. Код коллекции выполняется раньше кода запроса, поэтому код коллекции нужно обернуть для возможности вызова из кода коллекции.
pm.globals.set('loadUtils', function loadUtils() {
let utils = {};
utils.signBody = function signBody() {
sign = CryptoJS.SHA256(
pm.variables.get("vpbx_api_key") +
pm.variables.get("json") +
pm.variables.get("vpbx_api_salt")
).toString();
pm.variables.set(
"content_body",
"vpbx_api_key=" + pm.variables.get("vpbx_api_key") + "&" +
"sign=" + sign + "&" +
"json=" + encodeURI(pm.variables.get("json"))
);
}
return utils;
} + '; loadUtils();');
var json = {
data: [
{
"type": 0,
"name": "name",
"phones": [
{"phone": ""}
]
}
]
};
pm.variables.set("json", JSON.stringify(json));
const utils = eval(globals.loadUtils);
utils.signBody();
Описание варианта с Oauth аутентификацией используя сервисного пользователя
Для работы с сервисом нужен токен доступа, для получения которого нужно отправить JWT запрос, у Google применяется формат JWT RS 256, в котором используется RSA.
Решение варианта с Oauth аутентификацией используя сервисного пользователя
Реализовать получение токена в обработчике перед запросом. Кешировать токен в переменных окружения с фиксацией срока действия.
Решение нашел по адресу https://gist.github.com/dinvlad/425a072c8d23c1895e9d345b67909af0
/* This script auto-generates a Google OAuth token from a Service Account key,
* and stores that token in accessToken variable in Postman.
*
* Prior to invoking it, please paste the contents of the key JSON
* into serviceAccountKey variable in a Postman environment.
*
* Then, paste the script into the "Pre-request Script" section
* of a Postman request or collection.
*
* The script will cache and reuse the token until it's within
* a margin of expiration defined in EXPIRES_MARGIN.
*
* Thanks to:
* https://paw.cloud/docs/examples/google-service-apis
* https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests
* https://gist.github.com/madebysid/b57985b0649d3407a7aa9de1bd327990
* https://github.com/postmanlabs/postman-app-support/issues/1607#issuecomment-401611119
*/
const ENV_SERVICE_ACCOUNT_KEY = 'serviceAccountKey';
const ENV_JS_RSA_SIGN = 'jsrsasign';
const ENV_TOKEN_EXPIRES_AT = 'tokenExpiresAt';
const ENV_ACCESS_TOKEN = 'accessToken';
const JS_RSA_SIGN_SRC = 'https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js';
const GOOGLE_OAUTH = 'https://www.googleapis.com/oauth2/v4/token';
// add/remove your own scopes as needed
const SCOPES = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
];
const EXPIRES_MARGIN = 300; // seconds before expiration
const getEnv = name =>
pm.environment.get(name);
const setEnv = (name, value) =>
pm.environment.set(name, value);
const getJWS = callback => {
// workaround for compatibility with jsrsasign
const navigator = {};
const window = {};
let jsrsasign = getEnv(ENV_JS_RSA_SIGN);
if (jsrsasign) {
eval(jsrsasign);
return callback(null, KJUR.jws.JWS);
}
pm.sendRequest(JS_RSA_SIGN_SRC, (err, res) => {
if (err) return callback(err);
jsrsasign = res.text();
setEnv(ENV_JS_RSA_SIGN, jsrsasign);
eval(jsrsasign);
callback(null, KJUR.jws.JWS);
});
};
const getJwt = ({ client_email, private_key }, iat, callback) => {
getJWS((err, JWS) => {
if (err) return callback(err);
const header = {
typ: 'JWT',
alg: 'RS256',
};
const exp = iat + 3600;
const payload = {
aud: GOOGLE_OAUTH,
iss: client_email,
scope: SCOPES.join(' '),
iat,
exp,
};
const jwt = JWS.sign(null, header, payload, private_key);
callback(null, jwt, exp);
});
};
const getToken = (serviceAccountKey, callback) => {
const now = Math.floor(Date.now() / 1000);
if (now + EXPIRES_MARGIN < getEnv(ENV_TOKEN_EXPIRES_AT)) {
return callback();
}
getJwt(serviceAccountKey, now, (err, jwt, exp) => {
if (err) return callback(err);
const req = {
url: GOOGLE_OAUTH,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
mode: 'urlencoded',
urlencoded: [{
key: 'grant_type',
value: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
},{
key: 'assertion',
value: jwt,
}],
},
};
pm.sendRequest(req, (err, res) => {
if (err) return callback(err);
const accessToken = res.json().access_token;
setEnv(ENV_ACCESS_TOKEN, accessToken);
setEnv(ENV_TOKEN_EXPIRES_AT, exp);
callback();
});
});
};
const getServiceAccountKey = callback => {
try {
const keyMaterial = getEnv(ENV_SERVICE_ACCOUNT_KEY);
const serviceAccountKey = JSON.parse(keyMaterial);
callback(null, serviceAccountKey);
} catch (err) {
callback(err);
}
};
getServiceAccountKey((err, serviceAccountKey) => {
if (err) throw err;
getToken(serviceAccountKey, err => {
if (err) throw err;
});
});
Для корректной работы в переменную "serviceAccountKey" нужно записать содержимое JSON ключа созданного для сервисного пользователя.
В скрипте скачивается внешний JS скрипт и кешируется в переменной "jsrsasign".
Ключ выдается под определенные права доступа (scopes), но в переменных не фиксируется на какие права был выдан токен, поэтому при использовании сервиса с другими правами доступа нужно вручную удалить токен и срок действия из переменных для получения нового токена.
При наличии нескольких коллекций под разные сервисы переменные окружения (pm.environment) можно перевести в переменные коллекции (pm.collectionVariables).
Описание варианта с Oauth аутентификацией с ручным получением ключа
Формируем GET запрос, переносим в браузер, на уровне браузера подтверждаем права доступа, получаем код.
Используем код для получения access и refresh токенов
Решение варианта с Oauth аутентификацией с ручным получением ключа
Формируем GET запрос с параметрами, запускаем, берем из консоли строку запроса. берем первую, потому что остальные это автообработка редиректов postman-ом .
Полученный код добавляем в переменные коллекции и выполняем запрос на получение ключа, в вкладке tests добавляем скрипт обработки полученных данных
pm.test("access_token", () => {
let token_ok = false;
if (pm.expect(pm.response.code).to.eql(200)) {
const responseJson = pm.response.json();
if (pm.expect(responseJson.access_token).to.be.a('string')) {
pm.collectionVariables.set("access_token", responseJson.access_token);
token_ok = true;
};
if (pm.expect(responseJson.refresh_token).to.be.a('string')) {
pm.collectionVariables.set("refresh_token", responseJson.refresh_token);
token_ok = true;
};
return token_ok;
};
});
В результате успешного выполнения скрипта заполнились значения переменных с ключами, которые можно использовать в дальнейшей работе в рамках коллекции.
Описание варианта с JWT HS256 токеном
При каждом запросе формируется JWT токен с ключом и датой окончания действия, который помещается в заголовок запроса
Решение варианта с JWT HS256 токеном
function base64url(source) {
// Encode in classical base64
encodedSource = CryptoJS.enc.Base64.stringify(source);
// Remove padding equal characters
encodedSource = encodedSource.replace(/=+$/, '');
// Replace characters according to base64url specifications
encodedSource = encodedSource.replace(/\+/g, '-');
encodedSource = encodedSource.replace(/\//g, '_');
return encodedSource;
}
var header = {
"typ": "JWT",
"alg": "HS256"
};
var data = {
"iss": pm.collectionVariables.get("api_key"),
};
data.exp = Math.floor(Date.now() / 1000) + 3600;
var secret = pm.collectionVariables.get("api_secret");
// encode header
var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);
// encode data
var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);
// build token
var token = encodedHeader + "." + encodedData;
// sign token
var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);
var signedToken = token + "." + signature;
pm.variables.set("jwt_token", signedToken);
Итог
Postman удобный инструмент для тестирования API, работа с ним при использовании скриптов становится более производительной.
Благодарю за внимание.