Azure Blob Download with AAD Authentication

Standard

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.

Azure Blob Download with AAD Authentication

Happy coding… 🙂

Bei Fragen fragen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website ist durch reCAPTCHA geschützt und es gelten die Datenschutzbestimmungen und Nutzungsbedingungen von Google