2
votes

I've been struggling with what appears to be a common problem: formatting my authorization header for the Azure Table Service REST API. I have been unable to find an example using PowerShell and SharedKey, and am worried that I am making some dumb mistake in working backwards from other examples.

The specific (though unspecific) error is: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

I have been referencing these articles, as well as other examples:

I've confirmed that my key is correct, the table exists, has at least one row, and a number of other suggestions without luck.

If there is another approach I should take to accomplish the same goal, I'm all ears. I would like to use the REST API to allow for maximum compatibility on the clients.

$tableEndpoint = 'https://STORAGEACCOUNTNAME.table.core.windows.net/'
$tableName = 'TABLENAME'
$StorageAccountName = 'STORAGEACCOUNTNAME'
$Key = "STORAGEACCOUNTKEY"

Function New-AuthorizationHeader
{
    param ($canonicalizedString)

    [byte[]]$Bytes = [system.convert]::FromBase64String($Key)
    $HMACSHA256 = [System.Security.Cryptography.HMACSHA256]::new($Bytes)
    $dataToHmac = [System.Text.Encoding]::UTF8.GetBytes($canonicalizedString)
    $Signature = [System.Convert]::ToBase64String($HMACSHA256.ComputeHash($dataToHmac))

    [string]$AuthorizationHeader = "{0} {1}:{2}" -f "SharedKey",$StorageAccountName,$Signature

    $AuthorizationHeader
}

Function New-Entity
{
    param ($jsonContent)

    $requestMethod = "POST"
    $contentMD5 = [string]::Empty
    $storageServiceVersion = '2016-05-31'
    $reqDate = (Get-Date -Format r)
    $contentType = "application/json"
    $canonicalizedResource = "/{0}/{1}" -f $StorageAccountName,($tableEndpoint + $tableName)

    $stringToSign = "{0}`n{1}`n{2}`n{3}`n{4}" -f $requestMethod,$contentMD5,$contentType,$reqDate,$canonicalizedResource

    $authorizationHeader = New-AuthorizationHeader -canonicalizedString $stringToSign
    $content = [System.Text.Encoding]::UTF8.GetBytes($jsonContent)

    $fullURI = New-Object -TypeName System.Uri -ArgumentList ($tableEndpoint + $tableName)
    $httpWebRequest = [System.Net.WebRequest]::Create($fullURI)
    $httpWebRequest.Accept = 'application/json;odata=fullmetadata'
    $httpWebRequest.ContentLength = $content.length
    $httpWebRequest.ContentType = $contentType
    $httpWebRequest.Method = $requestMethod
    $httpWebRequest.Headers.Add("x-ms-date", $reqDate)
    $httpWebRequest.Headers.Add("x-ms-version", $storageServiceVersion)
    $httpWebRequest.Headers.Add("Authorization", $authorizationHeader)
    $httpWebRequest.Headers.Add("Accept-Charset", "UTF-8")
    $httpWebRequest.Headers.Add("DataServiceVersion", "3.0;NetFx")
    $httpWebRequest.Headers.Add("MaxDataServiceVersion", "3.0;NetFx")

    $requestStream = $httpWebRequest.GetRequestStream()
    $requestStream.Write($content, 0, $content.length)
    $requestStream.Close()

    $response = $httpWebRequest.GetResponse()
    $dataStream = $response.GetResponseStream()

    $reader = New-Object -TypeName System.IO.StreamReader($dataStream)

    $responseFromServer = $reader.ReadToEnd()
}

$jsonContent = @"
{
    "ExecutionStatus"="smapledata",
    "PartitionKey"="$ENV:Username",
    "RowKey"="PrinterScript"
}
"@

New-Entity -jsonContent $jsonContent
2

2 Answers

0
votes

Please make 2 changes above:

  1. Canonical resource should not have table endpoint. So it should be:

    $canonicalizedResource = "/{0}/{1}" -f $StorageAccountName,$tableName

  2. JSON body should be properly formatted. So it should be:

    $jsonContent = @" { "ExecutionStatus":"smapledata", "PartitionKey":"$ENV:Username", "RowKey":"PrinterScript" } "@

Once you make these changes, the code should work just fine.

0
votes

Thank you again for your responses, Gaurav.

I verified that my clock is not skewed. In the end, I switched to using a Shared Access Signature, which is probably a better practice anyway.

Using a SAS obviates the need for an Authorization header (and getting that correctly formatted).

Here's the relevant updated PowerShell:

$tableEndpoint = 'https://STORAGEACCOUNT.table.core.windows.net/'
$tableName = 'TABLENAME'
$SAS = "?sv=2016-05-31&ss=t&srt=o&sp=wa&se=2017-09-01T04:08:11Z&st=2017-03-14T20:08:11Z&spr=https&sig=SIGNATURE"
$URI = $tableEndpoint + $tableName + $SAS
If (-NOT $script:RunLogKeyTime) 
{
    $script:RunLogKeyTime = (Get-Date -Format 'yyyyMMdd-HHmmss')
}

$RequestBody = ConvertTo-Json -InputObject @{
        "TagetName"= $TargetName;
        "Message"= $Message;
        "ComputerName"= $ENV:ComputerName;
        "Username"= $ENV:Username;
        "EntryType"= $EntryType;
        "PartitionKey"= "$Username`_$ScriptIdentifier";
        "RowKey"= "$EntryType"}

$EncodedRequestBody = [System.Text.Encoding]::UTF8.GetBytes($RequestBody)

$RequestHeaders = @{
    "x-ms-date"=(Get-Date -Format r);
    "x-ms-version"="2016-05-31";
    "Accept-Charset"="UTF-8";
    "DataServiceVersion"="3.0;NetFx";
    "MaxDataServiceVersion"="3.0;NetFx";
    "Accept"="application/json;odata=nometadata";
    "ContentLength"=$EncodedRequestBody.Length}

Invoke-WebRequest -Method POST -Uri $URI -Headers $RequestHeaders -Body $EncodedRequestBody -ContentType "application/json"