Skip to main content

Authorization

What's the idea? The merchant sends requests to us. Therefore, we need to verify them to ensure that the request was indeed made by the merchant. Similarly, in the opposite direction: We send requests to the merchant, and they need to verify that we are the actual sender.

How is it implemented? Using pairs of public and private keys and request signing.

During merchant registration, the system generates four keys:

  • FirstPay Public Key
  • FirstPay Private Key
  • Merchant Public Key
  • Merchant Private Key

Afterward, the merchant receives the FirstPay public key and their own private key.

When the merchant sends a request to us, they sign it using their private key. Since we have the merchant’s public key, we can verify the authenticity of the request.

The same logic applies in the opposite direction: When we (FirstPay) send requests to the merchant, we sign them using the FirstPay private key. The merchant verifies the signature using the known FirstPay public key.

More detailed implementation scenarios can be found below.

This is the current authorization format that must be used during registration. If you are using a deprecated authorization method, information about it is available here.

Outgoing Requests

To verify all outgoing requests, several steps must be performed:

  1. Create the request object – the body of the request.
  2. Add the publicKey field to the object — the public key issued by FirstPay.
  3. Convert the body to a string using a specific algorithm.
  4. Encode the resulting string in Base64.
  5. Sign the Base64-encoded string using the merchant's private key with the SHA256 algorithm.
  6. Convert the resulting signature to Base64.
  7. Add a new field hash to the original request object, with its value set to the signature result.
  8. The request is now ready to be sent.

Example TypeScript code:

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

Incoming Requests

To verify all incoming requests, the following steps must be performed:

  1. Check for the presence of the hash field.
  2. Separate the request body from the hash field.
  3. Verify the hash field using the remaining object and the FirstPay public key:
  • Prepare the object following steps 3–5 from the previous section.
  • Verify the hash value using the SHA256 algorithm.
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

Algorithm

The prepareMessage function includes a recursively called function stringify, which operates according to the following logic:

  1. Array check.

    1.1. If the array is empty, return the string '[]'.

    1.2. If the array contains elements, recursively call stringify on each element and concatenate the results into a single string, separated by a pipe symbol '|'.

  2. Object check.

    2.1. If the object is empty, return the string ''.

    2.2. If the object contains fields, recursively call stringify on each field and concatenate the results into a single string, separated by '|'.

  3. All other cases.

    3.1. Always return the value converted to a string.

Additional Notes:

  1. In our implementation, within the object check, we include an extra condition: 'value !== null'. This is necessary in JavaScript because typeof null === 'object'. If you're implementing this logic in another language, this check may not be necessary.

  2. Working with keys. The stringify function takes the current key (i.e. the full path to the value) as its first argument. This key is dynamically built based on the structure:

    2.1. When processing a non-empty array, append the current index to the key for each element.

    2.2. When processing a non-empty object, append the field name to the key for each field.

    2.3. In all other cases, the key remains unchanged.

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);
}