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:
- Create the request object – the
bodyof the request. - Add the
publicKeyfield to the object — the public key issued by FirstPay. - Convert the body to a string using a specific algorithm.
- Encode the resulting string in Base64.
- Sign the Base64-encoded string using the merchant's private key with the SHA256 algorithm.
- Convert the resulting signature to Base64.
- Add a new field
hashto the original request object, with its value set to the signature result. - 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:
- Check for the presence of the
hashfield. - Separate the request
bodyfrom thehashfield. - 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
hashvalue 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:
-
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 '|'.
-
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 '|'.
-
All other cases.
3.1. Always return the value converted to a string.
Additional Notes:
-
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.
-
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);
}