Azure - Generate Azure Storage SAS Token using Powershell Functions & Managed Identity

azure blog function-apps powershell SAS azure-storage managed-identity security serverless

Image by Max Bender on Unsplash

In my last post, Azure Functions - Securing resources with a function proxy, I talked about my work to develop a more Azure-centric solution to replace my current home security camera automation workflow.

Another link in the chain for me was to be able to serve up a webpage blob from my container whilst keeping the Container Access Policy set to Private.

The function proxy in the previous post enabled that, but I also then needed the webpage to be able to retrieve the video/image blobs uploaded to display them.

Background

So, I needed to be able to use or generate and use a Shared Access Signature (SAS) in the URL when retrieving the blobs.

Now, I developed this before the function proxy, so I'm generating a SAS here and yet using one stored in an application setting in the function proxy - my plan would therefore be to combine the two into the one function app and not hard-code a SAS token - that will come later.

Back to the problem at hand - in the Javascript in my webpage blob, I can use the XMLHttpRequest WEB API to call an API to get a SAS token (it's how I retrieve the list of blobs in the webpage using the Azure Blob Service REST API).

Therefore, a powershell function seemed the obvious fit given my previous use of Powershell Functions and my love of Powershell generally.

Where to Begin

The first thing we need is a powershell function app - follow this excellent Microsoft Docs Quickstart Guide to get started.

Once you have a powershell function app with an HTTP trigger - you'll want to replace the code in run.ps1 with the following code (also available in my Bitbucket Repo) - if you're doing this in the portal then do it here:

Function App code Portal

If you're using VS Code then here: VS Code Editor

Now, let's take a look at my code:

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."

The above is just boilerplate, produced by VS Code when you create a new powershell function

Now we get into some specifics - I wanted to have the SAS token generated only be valid for 2 hours (so if someone somehow captured it even though we are using https throughout, we limit how long they have access for - in practice I will probably reduce the active time further still), so we need to do some date manipulation:

$StartTime = Get-Date
$EndTime = $startTime.AddHours(2.0)

Next - check if we have a token already stored in the SAST environment variable in the function app. If so, check if it's due to expire in the next 300 seconds. Only if it doesn't exist or is about to expire, generate a new SAS token.

If ($env:SAST.Length -gt 0) {
    $endt=($env:SAST).Split('&')[4].Split('=')[1] -replace '%3A',':'
    If ((([DateTime]$endt - (Get-Date)).TotalSeconds -gt 0) -and (([DateTime]$endt - (Get-Date)).TotalSeconds -gt 300)) {
            $GetNewToken = $false
            Write-Host "Valid Token Found"
    } else {
        $GetNewToken = $true
        Write-Host "Existing token expired or about to expire - generating new token"
    }
} else {
    $GetNewToken = $true
    Write-Host "No token found - generating new token"
}

If we decided to generate a new token, execute the code below, which uses the Get-AzStorageAccount cmdlet and the New-AzStorageContainerSASToken cmdlet.

I'm hardcoding the account name, resource group name and container name here but you could store them in KeyVault or app settings if you prefer or even parameterise this function so it has more widespread usability for you - this is just a quick Proof of Concept. If using this code as is - substitute the following with your own values:

  • <acct_name>
  • <rg_name>
  • <container_name>

I've also set the Permission option to rl which will allow read and list actions for the blobs in the named container.

If ($GetNewToken) {
    $rslt = Get-AzStorageAccount -Name "<acct_name>" -ResourceGroupName "<rg_name>" | New-AzStorageContainerSASToken  -Container "<container_name>" -Permission rl -StartTime $StartTime -ExpiryTime $EndTime
    [Environment]::SetEnvironmentVariable("SAST",$rslt.TrimStart("?"))
    Write-Host "New token generated - will expire in 2 hours"
    $status = [HttpStatusCode]::OK
    $body = $rslt
} else {
    $status = [HttpStatusCode]::OK
    $body = $env:SAST
}

The rest of the code is again boilerplate.

# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

Next, lets enable Managed Identity in the function app:

  1. Click on Platform Features

  2. Click on Identity under the Networking heading

  3. Click on On to enable the Managed Identity then click on Save

  4. Navigate to the storage account that you are working with
  5. Click on Blobs

  6. Click on the container you want to enable access to
  7. Click on Access Control (IAM) in the left-hand pane

  8. Click on Add in the Add a role assignment box

  9. In the Assign access to field, select Function App

  10. Select your function app from the list, or start to type its name in the Select field to narrow the list
  11. Select a role - in this case we want Storage Account Contributor

  12. Click on Save

Now - once the code is published to the function app - we can run/test it.

From the portal:

  • Click on the Function name under the Functions heading.
  • Because we don't pass any input parameters to the code as is, we can just click on Run:

  • After a few seconds you should see:

Et voila - a SAS token returned in the output from the HTTP call.

My code could be embellished, made more complicated, more robust etc - but you get the idea!

Summary

With minimal code, and minimal effort, I can programmatically generate a SAS token, with an expiration period, and easily access it from within my application code.

I'm sure you can see how useful this could be - and of course it's just one more example of the power of Azure Powershell Functions (other languages are available!)

As ever, thanks for reading and feel free to leave comments below.

Blog Comments powered by Disqus.

Previous Post Next Post