Create downloadable blob links with Azure Functions and App Service authentication
I recently had a situation where we wanted to provide a downloadable link to a file in Azure Blob Storage, but for that link to require that you authenticate with Azure Active Directory in order to download the file.
I'm not aware of any built-in capabilities of blob storage that let you generate such a link. However, with Azure Functions and the App Service Authentication feature (sometimes known as "Easy Auth"), it's relatively easy to build this.
My approach was simple. First, create a HTTP triggered Azure function that returns a redirect response to a short-lived SAS URL (only needs to be long enough to download the file). Of course I could have made the function return the contents of the target blob directly, but for performance reasons I didn't want to do that.
Second, the Azure Function App should use App Service Authentication to only allow AD authenticated callers in a specific Azure tenant.
Implementing the Azure Function
Here's the code for the Azure Function...
public static class MyFunctions
{
private static BlobServiceClient client;
[FunctionName(nameof(GetDownloadLink))]
public static async Task<IActionResult> GetDownloadLink(
[HttpTrigger(AuthorizationLevel.Anonymous, "get",
Route = "download/{container}/{*path}")] HttpRequest req,
string container, string path, ILogger log)
{
log.LogInformation($"requesting SAS URI for {path} in {container}");
ClaimsPrincipal identities = req.HttpContext.User;
if (identities != null)
{
var claims = string.Join(',',
identities.Claims.Select(c => $"{c.Type}:{c.Value}"));
log.LogInformation($"requested by {identities.Identity.Name} with {claims}");
}
if (client == null)
{
var connectionString = Environment.GetEnvironmentVariable("TargetStorage");
client = new BlobServiceClient(connectionString);
}
// put whatever restrictions you want here to only expose the parts of the
// storage account you want to be accessible
if (container != "mycontainer")
{
return new UnauthorizedResult();
}
var containerClient = client.GetBlobContainerClient(container);
if (!await containerClient.ExistsAsync())
{
return new NotFoundObjectResult($"Container {container} not found");
}
var blobClient = containerClient.GetBlobClient(path);
if (!await blobClient.ExistsAsync())
{
return new NotFoundObjectResult($"Blob {path} not found in {container}");
}
var builder = new BlobSasBuilder(BlobSasPermissions.Read,
DateTimeOffset.Now.AddMinutes(10));
var sasUri = blobClient.GenerateSasUri(builder);
return new RedirectResult(sasUri.ToString(), false);
}
The Azure function itself uses anonymous authorization because we'll be enabling App Service Authentication.
The function Route
uses wildcards that allow us to specify a container name and path in the target storage account. Obviously, this allows any file in that storage account to be downloaded, so you could either constrain the containers you allow (like I show in my example), or even use a simple database (e.g. Table Storage) that maps from a download name to a location in blob storage if you want to really lock it down to specific downloadable files.
To generate the SAS URI, I create a BlobServiceClient
using a Storage Account connection string from the Function App's App Settings, and then call the GenerateSasUri
to make a 10 minute SAS URI that's returned with a RedirectResult
. This means that users who click this link will be redirected in their browser directly to the SAS URI and download the file (or show it in the browser if its something like an image).
I also show in the code sample how you can examine the claims principal of the authenticated user, which would be useful if you needed to audit who had accessed the file, or place further restrictions on who is allowed to download it.
Enabling App Service Authentication
Enabling App Service Authentication can be automated (e.g. see an example here), but is a bit tricky as it involves creating an App Registration. I opted simply to use the portal for my proof of concept application.
In the portal, you simply navigate to your Function App, and go to the "Authentication" link on the right-hand side. And then select "add an identity provider"...
This will walk you through a wizard that takes you step by step through enabling App Service Authentication, and adding an identity provider.
For my identity provider I chose Microsoft (Azure Active Directory), and configured it to only allow logins from the "Current tenant - Single tenant", as I only want people in that AD tenant to be able to download the files. I allowed it to create a new App Registration for me. I chose to restrict access to require authentication, and use 302 for redirects as people will be accessing these links with a browser. For everything else, including the permissions I simply accepted the defaults.
Once it's complete, your authentication settings in the portal should look like this:
Setting | Value |
---|---|
App Service authentication | Enabled |
Restrict access | Require authentication |
Unauthenticated Request | Return HTTP 302 Found (Redirect to identity provider) |
Redirect to | Microsoft |
Token Store | Enabled |
Testing it out
Once you've configured App Service authentication, you can try it out by making a call to a URL similar to https://myfunctionapp.azurewebsites.net/
The first time you'll be prompted to grant permission the App Registration to see your basic info. Depending on your permissions in AD, you may also be able to accept this for the whole organization, which prevents others from needing to OK this dialog.
If you're already logged in as a user in the AD tenant, you'll immediately be redirected to the short-lived SAS URI and your browser will download (or display) the target file automatically. If you're not logged in (you can test this with an InPrivate browser window) you'll get prompted to log in before proceeding.
Summary
The Azure App Service authentication (Easy Auth) feature can make it very simple to restrict access to users in a particular AD tenant. My simple example obviously could be extended to be much more powerful with even more granular control over what files in the storage account could be downloaded, and restricting the users further (e.g. by checking for group membership).
Of course there may be an even easier way of creating a download link to an Azure Storage blob that requires AD authentication, so if you have better ideas, please let me know in the comments.