Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents

Scope

  • Access to private Azure Blob Storage requires authentication.
  • Shared Key authentication represents one of the authentication options.

Example

Download: azure_authentication_shared_key_sample.ps1


Code Block
languagepowershell
titleList, read and write blobs with Shared Key
linenumberstrue
collapsetrue
# inspired by:
#   https://stackoverflow.com/questions/53653473/generating-an-sas-token-in-java-to-download-a-file-in-an-azure-data-storage-cont?rq=1
#   https://purple.telstra.com.au/blog/loading-and-querying-data-in-azure-table-storage-using-powershell
#   https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key

function Create-Signature( [string] $Operation, [string] $Container, [string] $Blob, [string] $Account, [string] $AccessKey, [string] $Version, [DateTime] $Now, [int] $ContentLength=-1 )
{
    [Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null

    $partToSign =      "`n" `
                     + "`n" `
                     + "`n" `
                     + (($ContentLength -gt -1) ? $ContentLength : '') + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "`n" `
                     + "x-ms-blob-type:BlockBlob" + "`n" `
                     + "x-ms-date:$(Get-Date (Get-Date $Now).ToUniversalTime() -Format 'R')" + "`n" `
                     + "x-ms-version:$Version" + "`n"

    switch ( $Operation )                 
    {                 
        'GET'       {
                        $stringToSign  = 'GET' `
                                       + $partToSign `
                                       + "/$Account/$Container/$Blob"
                        break
                    }
                    
        'PUT'       {
                        $stringToSign  = 'PUT' `
                                       + $partToSign `
                                       + "/$Account/$Container/$Blob"
                        break
                    }
                    
        'LIST'      { 
                        $stringToSign  = "GET" `
                                       + $partToSign `
                                       + "/$Account/$Container" + "`n" `
                                       + "comp:list" + "`n" `
                                       + "restype:container"
                        break
                    }
                    
        default     {
                        throw "invalid operation: $Operation"
                    }
    }

    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    $hmac.key = [Convert]::FromBase64String( $AccessKey )
    
    $signature = $hmac.ComputeHash( [Text.Encoding]::UTF8.GetBytes( $stringToSign ) )
    $signature = [Convert]::ToBase64String( $signature )

    Write-Debug "container: $Container"
    Write-Debug "Accoount: $Account"
    Write-Debug "AccessKey: $AccessKey"
    Write-Debug "stringToSign: $stringToSign"

    $signature
}

function Invoke-BlobRequest( [Uri] $Uri, [string] $Account, [string] $Signature, [DateTime] $Now, [string] $Version, [string] $Method='GET', [string] $FilePath='' )
{
    $requestParams = @{}
    $requestParams.Add( 'Uri', $Uri )
    $requestParams.Add( 'Method', $Method )
    
    if ( $FilePath )
    {
        $requestParams.Add( 'Infile', $FilePath )
    }
    
    $requestParams.Add( 'Headers', @{ `
                                      'x-ms-blob-type' = 'BlockBlob'; `
                                      'x-ms-date' = "$(Get-Date (Get-Date $Now).ToUniversalTime() -Format 'R')"; `
                                      'x-ms-version' = $Version; `
                                      'Authorization' = "SharedKey $($Account):$($Signature)" `
                                    } `
                      )

    Write-Debug ".... Invoke-WebRequest: $Uri"
    
    $requestParams.Keys | % {
        if ( $_ -eq 'Headers' )
        {
            $item = $_
            $requestParams.Item($_).Keys | % {
                Write-Debug "...... Headers $_ : $($requestParams.Item($item).Item($_))"
            }
        } else {
            Write-Debug "...... $_  $($requestParams.Item($_))"
        }
    }

    # run the web service request
    $response = Invoke-WebRequest @requestParams
    $response
}


# ---------- ---------- ----------
# Main
# ---------- ---------- ----------

$ownerAccount     = '<storage account>'
$ownerAccessKey   = '<storage account access key>'
$requesterAccount = '<requester account>'
$version          = '2019-10-10'

# consider badly synchronized server time and start 3 minutes in the past
$now = (Get-Date).ToUniversalTime().AddMinutes(-3)


Write-Host "`n"
Write-Host "# ++++++++++++++++++++++++++++++"
Write-Host "# List Blobs in Container"
Write-Host "# ++++++++++++++++++++++++++++++"

# step 1: create signature
$container = 'yade'
$signature = Create-Signature -Operation 'LIST' -Container $container -Account $ownerAccount -AccessKey $ownerAccessKey -Version $version -Now $now

    Write-Debug "Signature: $signature"

# step 2: send web service request
$uri      = "https://$($ownerAccount).blob.core.windows.net/$($container)?restype=container&comp=list"
$response = Invoke-BlobRequest -Uri $uri -Account $requesterAccount -Signature $signature -Now $now -Version $version -Method 'GET'

    Write-Host "Status Code: $($response.StatusCode)"
    Write-Host $response.Content


Write-Host "`n"
Write-Host "# ++++++++++++++++++++++++++++++"
Write-Host "# Get Blob from Container"
Write-Host "# ++++++++++++++++++++++++++++++"

# step 1: create signature
$container = 'yade'
$blob      = 'test.txt'
$signature = Create-Signature -Operation 'GET' -Container $container -Blob $blob -Account $ownerAccount -AccessKey $ownerAccessKey -Version $version -Now $now

    Write-Debug "Signature: $signature"

# step 2: send web service request
$uri      = "https://$($ownerAccount).blob.core.windows.net/$($container)/$($blob)"
$response = Invoke-BlobRequest -Uri $uri -Account $requesterAccount -Signature $signature -Now $now -Version $version -Method 'GET'

    Write-Host "Status Code: $($response.StatusCode)"
    Write-Host $response.Content

Write-Host "`n"
Write-Host "# ++++++++++++++++++++++++++++++"
Write-Host "# Write Blob to Container"
Write-Host "# ++++++++++++++++++++++++++++++"

# step 1: create signature
$container = 'yade'
$filePath  = "c:/tmp/some_blob.txt"
$blob      = (Get-Item $filePath).Name
$signature = Create-Signature -Operation 'PUT' -Container $container -Blob $blob -Account $ownerAccount -AccessKey $ownerAccessKey -Version $version -Now $now -ContentLength (Get-Content $filePath -Raw).length

    Write-Debug "Signature: $signature"

# step 2: send web service request
$uri      = "https://$($ownerAccount).blob.core.windows.net/$($container)/$($blob)"
$response = Invoke-BlobRequest -Uri $uri -Account $requesterAccount -Signature $signature -Now $now -Version $version -Method 'PUT' -FilePath $filePath

    Write-Host "Status Code: $($response.StatusCode)"
    Write-Host $response.Content


Explanations

The Shared Key authentication enable unlimited access i.e it does not have any expiry. Refer to the Azure article Authorize with Shared Key for reference. The above PowerShell script creates the Shared Key for three operation LIST, GET, PUT. The three operation support three different process:

  1. LIST
    1. The LIST operation displays the list of blobs in the container. 
  2. GET
    1. The GET operation is used to get the content in a blob. 

  3. PUT
    1. The PUT operation is used to create a blob and adding a content to it or writing to a blob.

The Shared Key uses different parameters for different operation. The Shared Key requires a String which is to be encoded and need to passed as a Authorization Header in the request. 

The format for signature string is:

Code Block
titleStringToSign Format
StringToSign = VERB + "\n" +  
               Content-Encoding + "\n" +  
               Content-Language + "\n" +  
               Content-Length + "\n" +  
               Content-MD5 + "\n" +  
               Content-Type + "\n" +  
               Date + "\n" +  
               If-Modified-Since + "\n" +  
               If-Match + "\n" +  
               If-None-Match + "\n" +  
               If-Unmodified-Since + "\n" +  
               Range + "\n" +  
               CanonicalizedHeaders +   
               CanonicalizedResource;  


The StringToSIgn is then encoded using HMAC-SHA256 algorithm over the UTF-8-encoded signature string which is then used as an Authorization Header using syntax:

Code Block
titleAuthorization header format
Authorization="SharedKey <AccountName>:<Signature>"

where SharedKey is the authorization scheme, Account name is the name of account by which request is generated, Signature is the encoded StringToSign


The StringToSign parameters differ according to the operations:

  1. LIST BLOB:

    In the LIST BLOB operation lists the blobs in the container. The StringToSign for LIST BLOB will be:
     

    Code Block
    titleStringToSign for LIST BLOB operation
    $stringToSign  =     "GET" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + (($ContentLength -gt -1) ? $ContentLength : '') + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "x-ms-blob-type:BlockBlob" + "`n" `
                         + "x-ms-date:$(Get-Date (Get-Date $Now).ToUniversalTime() -Format 'R')" + "`n" `
                         + "x-ms-version:$Version" + "`n" `
                         + "/$Account/$Container" + "`n" `
                         + "comp:list" + "`n" `
                         + "restype:container"

    where $Account is the account by which request is generated. $Container is the name of the container.

  2. GET BLOB:

    The GTE BLOB operation displays the content of the blob. So, for the get operation it is required to pass the blob name whose content is to be read/get. So, the StringToSign for GET BLOB will be:

    Code Block
    titleStringToSign for GET BLOB operation
    $stringToSign  =     'GET' `
    					 + "`n" `
                         + "`n" `
                         + "`n" `
                         + (($ContentLength -gt -1) ? $ContentLength : '') + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "x-ms-blob-type:BlockBlob" + "`n" `
                         + "x-ms-date:$(Get-Date (Get-Date $Now).ToUniversalTime() -Format 'R')" + "`n" `
                         + "x-ms-version:$Version" + "`n" `
                         + "/$Account/$Container/$Blob"

    where $Account is the account by which request is generated. $Container is the name of the container. $Blob is the name of the blob whose content is to be Get.

  3. PUT BLOB

    The PUT BLOB operation creates a new block or updates an existing block blob. The PUT BLOB creates a BLOB of a length of content in a file so it is required to pass the length of the blob so that it can allocate a that much memory in the Container and can write the file content to the blob. The StringToSign for the PUT BLOB will be:

    Code Block
    titleStringToSign for PUT BLOB operation
    $stringToSign  = 	 'PUT' `
    				     + "`n" `
                         + "`n" `
                         + "`n" `
                         + (($ContentLength -gt -1) ? $ContentLength : '') + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "`n" `
                         + "x-ms-blob-type:BlockBlob" + "`n" `
                         + "x-ms-date:$(Get-Date (Get-Date $Now).ToUniversalTime() -Format 'R')" + "`n" `
                         + "x-ms-version:$Version" + "`n" `
                         + "/$Account/$Container/$Blob"

    where $Account is the account by which request is generated. $Container is the name of the container. $Blob is the name of the blob which is to be created. The $ContentLength for the LIST BLOB and GET BLOB operation will be empty but for the PUT BLOB it required to pass the Content Length for the file to be created.