An Azure blob download with AAD authentication isn’t available out of the box at the moment. You ever wondered about the missing feature authenticating an Azure Blob Download with your AAD (Azure AD)? Yes this feature is still not integrated into Azure Storage Account where your Blob resides. This feature would be very useful in cases where you only need to generate a temporary download link for a single file that needs to be downloaded by an internet user that is authenticated by your AAD.
Microsoft supports AAD authentication for blobs, but only supports easy data access from the Azure portal, Powershell and the CLI. Therefore I created this function to implement the missing pieces.
TL;DR
When the user clicks on the link the function will authenticate the user against your AAD generate a download link plus SAS token and redirect the browser to that temporary link.
What is the use case?
- You want to share files served by Azure Blob Storage
- The user need to be authenticated against your AAD
- Only A standard browser should be needed to download the file
- No fancy tooling allowed
The solution design
The main problem is that some process needs to authenticate the user. This can be accomplished with the help of an Azure function. I created the Azure function in powershell and created an ARM script that deploys the function to your Azure subscription.
The function has one input binding. The function takes the http request and takes the url parameter ‚file‘. Then it checks whether the blob exists and generates the SAS token. Look at the comments in the code. The function generates an output binding of the type http redirect (302). The browser of the user then redirects to the url. Easy.
The function source code is hosted in github and deployed automatically after deployment of the function has been finished.
The code for Azure Blob Download with AAD Authentication
Take a look into my repo. https://github.com/henrikmotzkus/BlobAAD
run.ps1
using namespace System.Net
# Input bindings are passed in via param block
param($Request, $TriggerMetadata)
# Interact with query parameters or the body of the request.
$file = $Request.Query.file
if (-not $file) {
$file = $Request.Body.file
}
try{
if ($file) {
# Getting settings
$saname = $env:APPSETTING_saname
$containername = $env:APPSETTING_containername
$key = $env:APPSETTING_sakey
#Generating a context for using in subsequent calls
$ctx = New-AzStorageContext -StorageAccountName $saname -StorageAccountKey $key
# Check if blob exists
$blob = Get-AzStorageBlob -Blob $file -Container $containername -Context $ctx -ErrorAction Stop
# Creating the SAS token
$StartTime = Get-Date
$EndTime = $startTime.AddMinutes(2.0)
$location = New-AzStorageBlobSASToken -Container $containername -Blob $file -Permission r -StartTime $StartTime -ExpiryTime $EndTime -FullUri -Context $ctx
}
}
Catch {
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::BadRequest
Body = "Nothing found"
})
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::Redirect
Headers = @{Location = $location}
})
ARM script: azuredeploy.json
This script deploys a function that is connected to your github repo. When the function is deployed the continious deployment feature will pull down the code to the function and take it productive.
When the neccessary storage account is deployed the its name and key will be added to the application settings of the function. The function itself need these application setting to get connected to the storage account from where you would server the blobs.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"repoURL": {
"type": "string",
"metadata": {
"description": "description"
}
},
"branch": {
"type": "string",
"metadata": {
"description": "description"
}
}
},
"functions": [],
"variables": {
"saname": "[uniqueString(resourceGroup().name)]",
"sfname": "[uniqueString(resourceGroup().name)]",
"functionAppName": "[uniqueString(resourceGroup().name)]",
"containername":"protected"
},
"resources":[
{
"name": "[variables('saname')]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"tags": {
"displayName": "[variables('saname')]"
},
"location": "[resourceGroup().location]",
"kind": "StorageV2",
"sku": {
"name": "Standard_LRS",
"tier": "Standard"
},
"resources": [
{
"type": "blobServices/containers",
"apiVersion": "2019-06-01",
"name": "[concat('default/', variables('containername'))]",
"dependsOn": [
"[variables('saname')]"
]
}
]
},
{
"name": "[variables('sfname')]",
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2018-02-01",
"location": "[resourceGroup().location]",
"sku": {
"name":"Y1",
"tier":"Dynamic",
"size":"Y1",
"family":"Y",
"capacity":0
},
"tags": {
"displayName": "[variables('sfname')]"
},
"properties": {
"name": "[variables('sfname')]"
}
},
{
"name": "[variables('functionAppName')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2018-11-01",
"location": "[resourceGroup().location]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('sfname'))]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('saname'))]"
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('sfname'))]",
"siteConfig": {
"appSettings": [
{
"name": "AzureWebJobsDashboard",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('saname'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('saname')),'2015-05-01-preview').key1)]"
},
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('saname'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('saname')),'2015-05-01-preview').key1)]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('saname'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('saname')),'2015-05-01-preview').key1)]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~3"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME",
"value": "powershell"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME_VERSION",
"value":"~7"
},
{
"name": "saname",
"value": "[variables('saname')]"
},
{
"name":"containername",
"value":"[variables('containername')]"
},
{
"name":"sakey",
"value":"[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('saname')),'2015-05-01-preview').key1]"
}
]
}
},
"resources": [
{
"apiVersion": "2018-11-01",
"name": "web",
"type": "sourcecontrols",
"dependsOn": [
"[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]"
],
"properties": {
"RepoUrl": "[parameters('repoURL')]",
"branch": "[parameters('branch')]",
"IsManualIntegration": true
}
}
]
}
],
"outputs": {}
}
Important
Take a look into the requirements.ps1. The function needs the ‚Az‘ powershell modules to function correctly.
After the deployment please activate the AAD integration with the portal.
Happy coding… 🙂