I am having some issues in creating a Discord bot. I want it to be able to respond to slash commands, but to do so I need to verify an endpoint. I am using PHP 7.4, and can't use any external libraries (hosting on a server that does not allow them). I have found documents for PHP, but they do require libraries to work. I tried taking the documents from Node.JS and "converting" them to PHP. Here's my code:
<?php
$public_key = "shh-it's-a-seekrit";
$headers = getallheaders();
$signature = $headers["X-Signature-Ed25519"];
$timestamp = $headers["X-Signature-Timestamp"];
$raw_body = file_get_contents('php://input');
/* To compute the signature, we need the following
* 1. Message ($timestamp + $body)
* 2. $signature
* 3. $public_key
* The algorythm is SHA-512
*/
$message = $timestamp . $raw_body;
$hash_signature = hash_hmac('sha512', $message, $public_key);
if (!hash_equals($signature, $hash_signature)) {
header("HTTP/1.1 401 Unauthorized", true, 401);
die("Request is not properly authorized!");
}
$return_array = [
'type' => 1,
];
echo json_encode($return_array);
?>
When I put the address the file is uploaded to and try to save the changes, Discord says the following:
Validation errors: interactions_endpoint_url: The specified interactions endpoint URL could not be verified.
This is a method that works for me on PHP 8.1.
Pass in the headers and raw JSON body, and it returns an array with the response code and payload to send back through whatever you are using to handle the response. Note the response must be JSON encoded.
$discord_public is the public key from the Discord app.
public function authorize(array $headers, string $body, string $discord_public): array
{
$res = [
'code' => 200,
'payload' => []
];
if (!isset($headers['x-signature-ed25519']) || !isset($headers['x-signature-timestamp'])) {
$res['code'] = 401;
return $res;
}
$signature = $headers['x-signature-ed25519'];
$timestamp = $headers['x-signature-timestamp'];
if (!trim($signature, '0..9A..Fa..f') == '') {
$res['code'] = 401;
return $res;
}
$message = $timestamp . $body;
$binary_signature = sodium_hex2bin($signature);
$binary_key = sodium_hex2bin($discord_public);
if (!sodium_crypto_sign_verify_detached($binary_signature, $message, $binary_key)) {
$res['code'] = 401;
return $res;
}
$payload = json_decode($body, true);
switch ($payload['type']) {
case 1:
$res['payload']['type'] = 1;
break;
case 2:
$res['payload']['type'] = 2;
break;
default:
$res['code'] = 400;
return $res;
}
return $res;
}
For completeness, adding the missing code to @Coder1 answer:
<?php
$payload = file_get_contents('php://input');
$result = endpointVerify($_SERVER, $payload, 'discord app public key');
http_response_code($result['code']);
echo json_encode($result['payload']);
function endpointVerify(array $headers, string $payload, string $publicKey): array
{
if (
!isset($headers['HTTP_X_SIGNATURE_ED25519'])
|| !isset($headers['HTTP_X_SIGNATURE_TIMESTAMP'])
)
return ['code' => 401, 'payload' => null];
$signature = $headers['HTTP_X_SIGNATURE_ED25519'];
$timestamp = $headers['HTTP_X_SIGNATURE_TIMESTAMP'];
if (!trim($signature, '0..9A..Fa..f') == '')
return ['code' => 401, 'payload' => null];
$message = $timestamp . $payload;
$binarySignature = sodium_hex2bin($signature);
$binaryKey = sodium_hex2bin($publicKey);
if (!sodium_crypto_sign_verify_detached($binarySignature, $message, $binaryKey))
return ['code' => 401, 'payload' => null];
$payload = json_decode($payload, true);
switch ($payload['type']) {
case 1:
return ['code' => 200, 'payload' => ['type' => 1]];
case 2:
return ['code' => 200, 'payload' => ['type' => 2]];
default:
return ['code' => 400, 'payload' => null];
}
}
According to Discord documentation, interactions endpoint must do two things:
The first part is very simple:
So, to properly ACK the payload, return a 200 response with a payload of type: 1:
Thus, you need to return JSON object and not an array (maybe that's one of the problems with your code).
Additionally, endpoint must be able to respond to invalid requests as:
We will also do automated, routine security checks against your endpoint, including purposefully sending you invalid signatures. If you fail the validation, we will remove your interactions URL in the future and alert you via email and System DM.
One thing I noticed is that Discord sends their headers lowercased! Another problem is that you use:
$raw_body = file_get_contents('php://input'); that doesn't look right.
Finally, if you know node.JS take a look at my working example: https://github.com/iaforek/discord-interactions