Powershell Invoke-WebRequest against Azure Storage Tables

To perform rest requests to azure storage tables you must sign your request, here is sample piece of code demonstrating how can it be done in Powershell:

$accountName = 'contoso'
$accountKey = '**************************************************************************************=='
$tableName = 'mytable'

$uri = "http://$accountName.table.core.windows.net/$tableName(PartitionKey='tasksSeattle',RowKey='1')"

$date = [datetime]::UtcNow.ToString('R', [System.Globalization.CultureInfo]::InvariantCulture)

$resource = [uri] $uri | select -ExpandProperty AbsolutePath # Path without query string

$stringToSign = $date + "`n/" + $accountName + $resource

$hasher = New-Object System.Security.Cryptography.HMACSHA256
$hasher.Key = [Convert]::FromBase64String($accountKey)
$signedSignature = [Convert]::ToBase64String($hasher.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign)))
$authorizationHeader = "SharedKeyLite " + $accountName + ":" + $signedSignature

$headers = @{
	Authorization = $authorizationHeader
	Date = $date
}

Write-Host 'Perform first request' -ForegroundColor Cyan
$response = Invoke-WebRequest -Uri $uri -Headers $headers
$response | select StatusCode, StatusDescription, @{n='ETag';e={ $_.Headers.ETag }}
$xml = [xml]$response.Content
$xml.entry.content.properties | fl

That code will return something like:

PartitionKey : tasksSeattle
RowKey       : 1
Timestamp    : Timestamp
Description  : Take out the trash.
DueDate      : DueDate
Location     : Home

Make sure to provide correct values, otherwise you will likely get Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signa ture. error.

Azure tables and conditional headers

It seems that Azure tables do not support conditional headers like If-Modified-Since like blobs do, but there is still one trick.

If you will make Invoke-WebRequest instead of Invoke-RestMethod you will also get response headers. And there will be ETag header - which is entity timestamp.

$response = Invoke-WebRequest -Uri $uri -Headers $headers
$response | select StatusCode, StatusDescription, @{n='ETag';e={ $_.Headers.ETag }} | fl
$xml = [xml]$response.Content
$xml.entry.content.properties | fl

Will return:

StatusCode        : 200
StatusDescription : OK
ETag              : W/"datetime'2015-12-11T10%3A53%3A52.6115577Z'"

PartitionKey : tasksSeattle
RowKey       : 1
Timestamp    : Timestamp
Description  : Take out the trash.
DueDate      : DueDate
Location     : Home

And now you can perform requests like:

$headers = @{
	Authorization = $authorizationHeader
	Date = $date
	'If-None-Match' = $response.Headers.ETag
}
Invoke-RestMethod -Uri $uri -Headers $headers

Which will give you desired (304) Not Modified.

Bootstraping example table from php

composer.json

{
  "repositories": [
    {
      "type": "pear",
      "url": "http://pear.php.net"
    }
  ],
  "require": {
    "pear-pear.php.net/mail_mime": "*",
    "pear-pear.php.net/http_request2": "*",
    "pear-pear.php.net/mail_mimedecode": "*",
    "microsoft/windowsazure": "*"
  }
}

sandbox.php

<?php
require_once 'vendor\autoload.php';
use WindowsAzure\Common\ServiceException;
use WindowsAzure\Common\ServicesBuilder;
use WindowsAzure\Table\Internal\ITable;
use WindowsAzure\Table\Models\EdmType;
use WindowsAzure\Table\Models\Entity;

$accountName = "contoso";
$accountKey = "**************************************************************************************==";
$connectionString = "DefaultEndpointsProtocol=https;AccountName=$accountName;AccountKey=$accountKey";

/** @var ITable $tableRestProxy */
$tableRestProxy = ServicesBuilder::getInstance()->createTableService($connectionString);

try {
	$createTableResult = $tableRestProxy->createTable("mytable");
} catch(ServiceException $ex) {
	if($ex->getCode() !== 409) throw $ex;
}

$entity = new Entity();
$entity->setPartitionKey("tasksSeattle");
$entity->setRowKey("1");
$entity->addProperty("Description", null, "Take out the trash.");
$entity->addProperty("DueDate", EdmType::DATETIME, new DateTime("2012-11-05T08:15:00-08:00"));
$entity->addProperty("Location", EdmType::STRING, "Home");

try {
	$tableRestProxy->insertEntity("mytable", $entity);
} catch(ServiceException $ex) {
	if($ex->getCode() !== 409) throw $ex;
}

Some links

ETag

Specifying Conditional Headers for Blob Service Operations

Authenticating against Azure Table Storage

Authentication for the Azure Storage Services

How to use table storage from PHP

Update from 2024

Here is one more example of how race conditions can be utilized with help of ETag

<#
docker run -it --rm --name=azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
#>

$accountName = 'devstoreaccount1'
$accountKey = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='
$tableName = 'mactemp'
# $endpoint = "https://$accountName.table.core.windows.net"
$endpoint = "http://127.0.0.1:10002/devstoreaccount1"

function repeated_signature_stuff_extracted_for_readability($uri) {
  $date = [datetime]::UtcNow.ToString('R', [System.Globalization.CultureInfo]::InvariantCulture)
  $resource = [uri] $uri | Select-Object -ExpandProperty AbsolutePath # Path without query string
  $stringToSign = $date + "`n/" + $accountName + $resource

  $hasher = New-Object System.Security.Cryptography.HMACSHA256
  $hasher.Key = [Convert]::FromBase64String($accountKey)
  $signedSignature = [Convert]::ToBase64String($hasher.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign)))
  $authorizationHeader = "SharedKeyLite " + $accountName + ":" + $signedSignature

  return @{
    Authorization  = $authorizationHeader
    Date           = $date
    'x-ms-version' = '2019-02-02'
    'Content-Type' = 'application/json'
    'Accept'       = 'application/json;odata=nometadata'
  }
}

# each example below, has repeating signature block, it is the same each time


# CREATE TABLE
$uri = "$endpoint/Tables"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{ TableName = $tableName })

# INSERT RECORD
$uri = "$endpoint/$tableName"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{
    PartitionKey = "pk1"
    RowKey       = "rk1"
    Foo          = "bar"
    Acme         = 42
  })


# RETRIEVE RECORD
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
# $response.Headers # Etag, Date
$response.Content | ConvertFrom-Json | Out-Host

# UPDATE RECORD
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{
    PartitionKey = "pk1"
    RowKey       = "rk1"
    Foo          = "BAZ"
    Acme         = 100500
  })

# RETRIEVE RECORD - check if it was updated
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$response.Content | ConvertFrom-Json | Out-Host


# --------------------------------------------

# DEMO RACE CONDITIONS

# lets pretend we have two clients, each trying to update the same record at the same time
# we are going to avoid race conditions by utilizing ETag and If-Match headers
# both clients will first read the record, then update it
# pseudo code will be:
# 1. client1 get record - ok
# 2. client2 get record - ok
# 3. client2 update record - ok
# 4. client1 update record - should fail - race condition


# 1. client1 get record - ok
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response1 = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$record1 = $response1.Content | ConvertFrom-Json
$record1 | Out-Host
$response1.Headers.ETag | Out-Host

# 2. client2 get record - ok
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response2 = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$record2 = $response2.Content | ConvertFrom-Json
$record2 | Out-Host
$response2.Headers.ETag | Out-Host

# 3. client2 update record - ok
$record2.Foo = "UPDATED BY CLIENT 2"
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$headers['If-Match'] = [string]$response2.Headers.ETag
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject $record2)

# 4. client1 update record - should fail - race condition
$record1.Foo = "UPDATED BY CLIENT 1"
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$headers['If-Match'] = [string]$response1.Headers.ETag
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject $record2)
# will fail with 412 Precondition Failed error saing that UpdateConditionNotSatisfied