Azure Blob Storage SAS reasoning via PowerShell Pester
12 Nov 2025
It turns out that once you’ve got PowerShell’s “Pester” module up and running on your computer, you don’t actually need to call Invoke-Pester to run code that includes syntax like Describe and It and | Should -Be. You can just run a normal .ps1 file and those commands simply … work … as if you’d run Invoke-Pester.
Neat!
I took advantage of this the other day when testing nuances of how shared access signatures (“SAS”) for “blobs” within containers of Azure Storage Accounts work in the real world.
Once I’d written up all of my Pester tests into a single .Tests.ps1 file, I was able to send a copy of that file to a stakeholder so that we could reason about whether we liked and trusted SAS “magic links” for a given tricky software problem we needed to solve.
I really enjoyed being able to put the implication of “Yes, I validated that it works as documented” and the assertion of “This is exactly how it works, in your jargon” into a single file. As opposed to, say, writing up a README.md file that said, “Okay, so, first, I tried ABC, and consequently, I saw XYZ in my console,” etc. It reminds me a lot of the reason Chris Moffitt touts Python and Pandas to his fellow Excel spreadsheet crunchers and scientists often publish their data-crunching research results through Jupyter notebooks: it makes reproducibility easier. Reproducibility leaves time for reasoning about what an experiment’s results mean, instead of debating whether they really happened.
Persistent blob URL works without headers
First, I validated that “normal” HTTP-based access to a given file’s persistent blob storage endpoint works as expected (using a Bearer token that I extracted from my logged-in Azure CLI session as part of an Authorization header accompanying the HTTP request).
Describe "Long YAML file exists via Bearer auth at persistent blob storage endpoint" {
BeforeAll {
$http_response = Invoke-WebRequest `
-Uri 'https://StorageAccountNameHere.blob.core.windows.net/iamacontainer/longfiles/LongYamlFile.yaml' `
-Method 'GET' `
-Headers (@{ `
'Authorization' = "Bearer $(az account get-access-token --resource 'https://storage.azure.com' --query 'accessToken' --output tsv)"
'x-ms-version' = '2018-03-28' `
}) `
| Select-Object -Property @('StatusCode', 'Content', 'Headers')
}
It "should be 200" {
$http_response.StatusCode | Should -Be 200
}
It "should have a sizeable body" {
$http_response.Content.Length | Should -BeGreaterThan 1000
$http_response.Content.Length | Should -BeLessThan 10000
}
It "should be a YAML file" {
$http_response.Headers['Content-Type'] | Should -Be 'application/yaml'
}
}
Persistent blob URL fails without headers
Next, I validated that “normal” HTTP-based access to a file’s persistent blob storage endpoint fails if I don’t bother to pass it a Bearer token at all when making the HTTP request.
Note that known-to-fail calls to Invoke-WebRequest have to be surrounded by try-catch blocks, because Invoke-WebRequest tries to “help” by throwing a PowerShell error if the HTTP response status code indicates that something went wrong with the HTTP request.
I learned through trial and error that you get a 409 HTTP response status code when you forget to include an authentication header at all when visiting an Azure Blob Storage endpoint URL, so that’s what I put into the | Should -Be assertion.
Describe "Long YAML file fails without auth at persistent blob storage endpoint" {
BeforeAll {
try {
Invoke-WebRequest `
-Uri 'https://StorageAccountNameHere.blob.core.windows.net/iamacontainer/longfiles/LongYamlFile.yaml' `
-Method 'GET'
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
$unauth_exception_status_code = $_.Exception.StatusCode
}
}
It "should be 409" {
$unauth_exception_status_code | Should -Be 409
}
}
Ephemeral SAS URL works without headers and times out properly
Once I felt confident that I had an understanding of persistent blob storage endpoint URL behavior when accessed over HTTP, I moved on to testing out whether SAS works as promised.
The idea is that a given SAS URL should work as a sort of short-lived “magic link” – one that doesn’t require me to attach any special headers into my HTTP request to get content back from it, but that I have to visit quickly after its creation, because it expires pretty soon.
- First, my script ran
az storage blob generate-sasto generate a magic link for my long YAML file. I set the--expiry10 seconds into the future. - Next, it ran
Invoke-WebRequestagainst that magic link twice, with a 10 secondStart-Sleepin between.- The idea was for the first
Invoke-WebRequestto succeed (despite the lack of an authentication header, becauase that’s the point of a magic link), and for the secondInvoke-WebRequestto fail. - (I cached out the results of each
Invoke-WebRequestinto its own variable, so I could size up the results later on in the script as| Should -Betests.) - (Tip: 10 seconds seemed to hit the sweet spot. Short enough for fast-running Pester tests, but long enough to avoid Azure clock skew bugs.)
- The idea was for the first
I learned through trial and error that you get a 403 HTTP response status code when you try to visit an Azure Blob Storage SAS magic link that’s already expired, and that the error message will contain the text “Signature not valid in the specified key time frame.” So that’s what I put into the | Should -Be assertion.
Describe "Long YAML file works and times out via SAS" {
BeforeAll {
$fast_expiring_magic_link = az storage blob generate-sas `
--account-name 'StorageAccountNameHere' `
--container-name 'iamacontainer' `
--name 'longfiles/LongYamlFile.yaml' `
--as-user `
--auth-mode 'login' `
--full-uri `
--https-only `
--content-type 'application/yaml' `
--permissions 'r' `
--start ((Get-Date).ToUniversalTime().AddSeconds(-10).ToString("yyyy-MM-ddTHH:mm:ssZ")) `
--expiry ((Get-Date).ToUniversalTime().AddSeconds(10).ToString("yyyy-MM-ddTHH:mm:ssZ")) `
--output 'tsv'
$http_response = Invoke-WebRequest `
-Uri $fast_expiring_magic_link `
-Method 'GET' `
| Select-Object -Property @('StatusCode', 'Content', 'Headers')
Start-Sleep `
-Seconds '10'
try {
Invoke-WebRequest `
-Uri $fast_expiring_magic_link `
-Method 'GET'
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
$unauth_exception_status_code = $_.Exception.StatusCode
$unauth_exception_message = $_.ErrorDetails.Message
}
}
Describe "Long YAML file works at SAS magic link upon first visit (before expiration)" {
It "should first be 200" {
$http_response.StatusCode | Should -Be 200
}
It "should first have a sizeable body" {
$http_response.Content.Length | Should -BeGreaterThan 1000
$http_response.Content.Length | Should -BeLessThan 10000
}
It "should first be a YAML file" {
$http_response.Headers['Content-Type'] | Should -Be 'application/yaml'
}
}
Describe "Long YAML file times out at SAS magic link upon second visit (after expiration)" {
It "should second be 403" {
$unauth_exception_status_code | Should -Be 403
}
It "should complain about a too-long expiration" {
$unauth_exception_message | Should -BeLike '*Signature not valid in the specified key time frame*'
}
}
}
Ephemeral SAS URL fails if its expiration violated policy
Next, I wanted to see if Azure’s feature of, at the Storage Account level, prohibiting any given az storage blob generate-sas command from using an --expiry property that’s “too long” really worked as advertised.
- At the
StorageAccountNameHereAzure Storage Account level, I’d set mine to have a maximum expiration of 1 minute. - In the Pester test below, my script
az storage blob generate-sasgreedily set--expiry3 hours into the future.
Surprisingly:
az storage blob generate-sasitself didn’t error out. It returned a normal-looking SAS “magic link” URL.- It wasn’t until I tried to visit the “magic link” URL that I finally received a
403HTTP response status code. 🤷♀️
Unlike the last 403, this one’s error message ended with the text “Policy violated by larger sas token interval.” So that’s what I put into the | Should -Be assertion.
Describe "Long YAML file never works when SAS expiration was too greedy" {
BeforeAll {
$fast_expiring_magic_link = az storage blob generate-sas `
--account-name 'StorageAccountNameHere' `
--container-name 'iamacontainer' `
--name 'longfiles/LongYamlFile.yaml' `
--as-user `
--auth-mode 'login' `
--full-uri `
--https-only `
--content-type 'application/yaml' `
--permissions 'r' `
--start ((Get-Date).ToUniversalTime().AddSeconds(-20).ToString("yyyy-MM-ddTHH:mm:ssZ")) `
--expiry ((Get-Date).ToUniversalTime().AddHours(3).ToString("yyyy-MM-ddTHH:mm:ssZ")) `
--output 'tsv'
try {
Invoke-WebRequest `
-Uri $fast_expiring_magic_link `
-Method 'GET'
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
$unauth_exception_status_code = $_.Exception.StatusCode
$unauth_exception_message = $_.ErrorDetails.Message
}
}
It "should be 403" {
$unauth_exception_status_code | Should -Be 403
}
It "should complain about a too-long expiration" {
$unauth_exception_message | Should -BeLike '*Policy violated by larger sas token interval.'
}
}