Search code examples
phpazurehmacazure-rest-api

Azure REST Api in PHP giving "Given value does not match HMAC header structure"


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=="
}

Solution

  • 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=="