Перейти к основному содержимому

Авторизация

Какая идея? Мерчант отправляет запросы нам. Как следствие нам необходимо их верифицировать, чтобы удостовериться, что именно мерчант был автором. Аналогично и в обратную сторону. Мы отправляем запросы мерчанту и с его стороны необходимо удостовериться, что именно мы отправители.

Как реализуем? С помощью пар публичных-приватных ключей и подписей запросов. Во время регистрации мерчанта в системе генерируются четыре ключа:

  • Публичный ключ Firstpay
  • Приватный ключ Firstpay
  • Публичный ключ мерчанта
  • Приватный ключ мерчанта

После чего мерчанту направляется публичный ключ Firstpay и приватный ключ мерчанта. Когда мерчант отправляет нам запрос, он подписывает его с помощью своего приватного ключа. В свою очередь мы, зная публичный ключ мерчанта, способны верифицировать автора.

Абсолютно аналогичная логика, когда в сценарии уже мы отправители, а мерчант - получатель. Мы с помощью приватного ключа Firstpay подписываем тела запроса. Мерчант, в свою очередь, верифицирует подпись с помощью известного ему публичного ключа Firstpay.

Более детальные сценарии по реализации можно увидеть ниже.

Это актуальный формат авторизации, который вам необходимо использовать при регистрации. Если вы использовали устаревший тип авторизации, информация о нем находится тут.

Исходящие запросы

Для верификации всех исходящих запросов необходимо выполнить несколько шагов.

  1. Сформировать объект - body запроса.
  2. Добавить в объект поле publicKey - выданный публичный ключ Firstpay.
  3. Привести тело к строке по специальному алгоритму.
  4. Закодировать полученную строку в base64.
  5. Полученную строку подписать с помощью приватного ключа мерчанта алгоритмом SHA256.
  6. Полученный результат также необходимо конвертировать в base64.
  7. В изначальный объект для запроса добавить поле hash, у которого значение - полученный результат.
  8. Запрос готов к отправке.

Пример кода на TS:

import {
createSign,
} from 'crypto';

const publicKey = '';
const privateKey = '';

function convertToBase64(value: string): string {
return Buffer
.from(value)
.toString('base64');
}

function getSignedHash(message: IObject, privateKey: string): string {
const preparedMessage = prepareMessage(message);
const encodedPreparedMessage = convertToBase64(preparedMessage);
const preparedPrivateKey = Buffer.from(privateKey, 'utf-8');

return createSign('SHA256')
.update(encodedPreparedMessage, 'base64')
.sign(preparedPrivateKey, 'base64');
}

const requestBody = {};

requestBody.publicKey = publicKey;
requestBody.hash = getSignedHash(requestBody, privateKey);

// Send request

Входящие запросы

Для верификации всех входящих запросов необходимо выполнить несколько шагов:

  1. Проверить наличие поля hash.
  2. Отделить body запроса от поля hash.
  3. Верифицировать поле hash с помощью оставшегося объекта и публичного ключа Firstpay.
  • Подготовить объект аналогично шагам 3-5 предыдущего раздела.
  • Верифицировать значение hash с помощью алгоритма SHA256.
import {
createVerify,
} from 'crypto';

const publicKey = '';
const privateKey = '';

function convertToBase64(value: string): string {
return Buffer
.from(value)
.toString('base64');
}

function verifySignedHash(message: IObject, signedHash: string, publicKey: string): boolean {
const preparedMessage = prepareMessage(message);
const encodedPreparedMessage = convertToBase64(preparedMessage);

return createVerify('SHA256')
.update(encodedPreparedMessage, 'base64')
.verify(publicKey, signedHash, 'base64');
}

const requestBody = {};

if (!requestBody.hash) {
console.log('No hash');
}

const {hash, ...restBody} = requestBody;

if (!verifySignedHash(restBody, hash, publicKey)) {
console.log('Invalid signature');
}

// Do something

Алгоритм

Функция prepareMessage содержит в себе рекурсивно вызывающуюся функцию stringify, которая работает по следующему принципу:

  1. Проверка на массив.

    1.1 Если массив пустой, то возвращаем строку '[]'.

    1.2. Если в массиве есть элементы, то для каждого элемента вызываем stringify и объединяем их всех в одну строку с разделителем '|'.

  2. Проверка на объект.

    2.1. Если объект пустой, то возвращаем строку ''.

    2.2. Если в объекте есть поля, то для каждого поля вызываем stringify и объединяем их всех в одну строку с разделителем '|'.

  3. Остальные случаи.

    3.1. Всегда возвращаем значение, приведённое к строке.

Нюансы:

  1. В нашем примере в проверке на объект есть дополнительное условие 'value !== null'. Эта проверка обусловлена тем, что typeof null === 'object' из-за специфики реализации JS. Если вы пишите на другом языке, вероятно, вам эту проверку добавлять необязательно.

  2. Работа с ключами. Функция stringify в качестве первого аргумента принимает текущий ключ - полный путь до значения:

    2.1. Если мы работаем с непустым массивом, то к каждому элементу в ключ добавляем его текущий индекс.

    2.2. Если мы работаем с непустым объектом, то к каждому полю в ключ добавляем название этого самого поля.

    2.3. В остальных случаях ключ никак не изменяется.

function prepareMessage(message: IObject): string {
function stringify(key: string, value: any): string {
switch (true) {
case Array.isArray(value): {
if (!value.length) {
return key ? `${key}=[]` : '[]';
}

return value
.map((item, index) => stringify(`${key}[${index}]`, item))
.join('|');
}
case typeof value === 'object' && value !== null: {
if (!Object.keys(value).length) {
return key ? `${key}={}` : '{}';
}

return Object
.keys(value)
.sort()
.map((childKey) => stringify(key ? `${key}.${childKey}` : childKey, value[childKey]))
.join('|');
}
default: {
return key ? `${key}=${value}` : `${value}`;
}
}
}

return stringify('', message);
}