1

I am trying to connect to the Azure Communications API to send SMS messages using the REST endpoint.

The link to the API instructions is here: https://learn.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-hmac

My PHP code is as follows:

    public static function send(array $phoneNumbers, string $message)
    {
        $body = json_encode([
            'from' => config("azure.sms_phone_number"),
            'message' => $message,
            'smsRecipients' => array_map(fn ($num) => ['to' => $num], $phoneNumbers)
        ]);

        $endpoint = parse_url(config("azure.sms_endpoint"));

        $headers = [
            'Date' => gmdate("D, d M y H:i:s T"),
            'host' => $endpoint['host'],
            'x-ms-content-sha256' => base64_encode(hash('sha256', $body, true)),
            'Content-Type' => 'application/json',
            'Accept' => '*/*'
        ];

        $stringToSign = utf8_encode("POST\n" . $endpoint['path'] . "?" . $endpoint['query'] . "\n" . implode(";", array_values($headers)));

        $headers['Authorization'] = implode("&", [
            "HMAC-SHA256 Credential=" . config("azure.sms_key_id"),
            "SignedHeaders=" . implode(";", array_keys($headers)),
            'Signature=' . base64_encode(hash_hmac('sha256', $stringToSign, base64_decode(config("azure.sms_key")), true))
        ]);

        $client = new Client();

        $client->post(config("azure.sms_endpoint"), [
            'headers' => $headers,
            'body' => $body,
            'debug' => true
        ]);
    }

Below are some of the variables

$body = '{"from":"+1844295xxx","message":"hi","smsRecipients":[{"to":"2019160xxx"}]};';

$stringToSign = 'POST
/sms?api-version=2021-03-07
Mon, 14 Mar 22 17:09:17 GMT;testxxxxxx.communication.azure.com;UXoK8141pppkVedAkc+eSQBqKOWciyoiq+AG/xxxxxx=;application/json;*/*;';

$headers = 
(
    [Date] => Mon, 14 Mar 22 17:09:17 GMT
    [host] => testxxxxx.communication.azure.com
    [x-ms-content-sha256] => UXoK8141pppkVedAkc+eSQBqKOWciyoiq+AG/xxxxxx=
    [Content-Type] => application/json
    [Accept] => */*
    [Authorization] => HMAC-SHA256 Credential=primaryKey&SignedHeaders=Date;host;x-ms-content-sha256;Content-Type;Accept&Signature=R8M7+fODzXaxXHbdcV5CHXiEq5R/7Fvd9VGYxxxxxxx=
)

The result I get is this:

Client error: `POST https://test.communication.azure.com/sms?api-version=2021-03-07` resulted in a `401 Unauthorized` response:
{"error":{"code":"Denied","message":"Given value does not match HMAC header structure."}}

The access keys and names come from this command:

PS C:\Users\manko> az communication list-key --name scotttestingsms --resource-group smsresourcegroup
{
  "primaryConnectionString": "endpoint=https://scotttestingsms.communication.azure.com/;accesskey=E7lrdL/yyg/snSO++rbaZMEUF/bC5/0R9XBVGcFclt3fEN/MpWRb5kHB9t59NLtek9xsUYXHyAXxxxxxxx==",
  "primaryKey": "E7lrdL/yyg/snSO++rbaZMEUF/bC5/0R9XBVGcFclt3fEN/MpWRb5kHB9t59NLtek9xsUYXHyAXxxxxxx==",
  "secondaryConnectionString": "endpoint=https://scotttestingsms.communication.azure.com/;accesskey=IIK094eGVfkNG0uFii/32j+HVsEHJ4/QUOx06TVsqwLub7A/cv1AKKnkkZQbKiJKMn/KRx9o1biWQ5txxxxxx==",
  "secondaryKey": "IIK094eGVfkNG0uFii/32j+HVsEHJ4/QUOx06TVsqwLub7A/cv1AKKnkkZQbKiJKMn/KRx9o1biWQ5txxxxxx=="
}
mankowitz
  • 1,864
  • 1
  • 14
  • 32
  • How is function `config()` defined? How is class `Client` defined? Are you sure your secret (or "password" as you call it) isn't Base64 encoded anymore? – AmigoJack Mar 11 '22 at 21:10
  • Sorry: someone edited my post and took out the text for secret and password. I can't be 100% sure, but the `sms_key` appears to be base64 encoded, while `sms_password` is not. `Client()` is a php guzzle client, a curl wrapper. – mankowitz Mar 11 '22 at 21:32
  • No, it doesn't. Just like all the other programming language examples do you also should consider to return the value Base64 decoded when calling the `config()` function. – AmigoJack Mar 12 '22 at 07:20

1 Answers1

2

It took quite some time of trial and error, but the following code works. See https://learn.microsoft.com/en-us/azure/communication-services/tutorials/hmac-header-tutorial for more information.

use GuzzleHttp\Client;

    public static function send(array $phoneNumbers, string $message)
    {
        $body = [
            'from' => config("azure.sms_phone_number"),
            'message' => $message,
            'smsRecipients' => array_map(fn ($num) => ['to' => $num], $phoneNumbers)
        ];

        $endpoint = parse_url(config("azure.sms_endpoint"));

        $headers = [
            'Date' => gmdate("D, d M Y H:i:s T"),
            'host' => $endpoint['host'],
            'x-ms-content-sha256' => base64_encode(hash('sha256', json_encode($body), true)),
        ];

        $stringToSign = utf8_encode(implode("\n", [
            "POST",
            $endpoint['path'] . "?" . $endpoint['query'],
            implode(";", array_values($headers))
        ]));

        $headers['Authorization'] = implode("&", [
            "HMAC-SHA256 SignedHeaders=" . implode(";", array_keys($headers)),
            'Signature=' . base64_encode(hash_hmac('sha256', $stringToSign, base64_decode(config("azure.sms_key")), true))
        ]);

        $client = new Client();  // <-- this is guzzle

        $response = $client->post(config("azure.sms_endpoint"), [
            'headers' => $headers,
            'json' => $body
        ]);

    }

You only need three pieces of data.

  1. config("azure.sms_phone_number") is the originating phone number. It must be in E.164 format, e.g. "+12024561414"
  2. config("azure.sms_endpoint") is the full endpoint, e.g. "https://test.communication.azure.com/sms?api-version=2021-03-07"
  3. config("azure.sms_key") is the application key copied right off the azure portal in base64, e.g. "E7lrdL/yyg/snSO++rbaZMEUF/d1G/0R9XBVGch0tq3xxxxxxxxxxxx=="
mankowitz
  • 1,864
  • 1
  • 14
  • 32
  • The only differences are removing `Credential=" . config("azure.sms_key_id")` from the header and using `'json' => $body` when posting. – AmigoJack Mar 21 '22 at 14:49
  • You are correct. In the document I referenced above, it said that you need to describe the Credential ID, which isn’t true. Also, the Content-Type header must be provided, but not hashed in the stringToSign. It was these little variations which prevented me from getting the API to work. – mankowitz Mar 22 '22 at 21:17