I'm Andrew Hoefling, and I work for FileOnQ as a Lead Software Engineer building mobile technologies for Government, Financial and First Responders using Xamarin. 

 

Uploading Files to Azure Blob Storage with Shared Access Signature (SAS)


Shared Access Signature (SAS) provides a secure way to upload and download files from Azure Blob Storage without sharing the connection string. A real world example would be to retrieve a Shared Access Signature on a mobile, desktop or any client side app to process the functions. This removes any need to share an all access connection string saved on a client app that can be hijacked by a bad actor.

In this article we will go over how to architect a client side applicaiton and backend service to securely use a Shared Access Signature (SAS) using a real world example

Real World Example

I am currently building a Universal Windows Platform (UWP) application that needs to upload files to an Azure Blob Storage resource. These files need to be uploaded by the client, but the client app should NEVER store the connection string or secrets of the Azure Resource. This would create a massive security hole where a bad actor could hijack the Azure Resource.

To implement this solution securely I have created a backend that provides a unique access token known as a Shared Access Signature (SAS) which shares that with the client. The client can now make the necessary requests to the Azure Resource. To continue with good security practices it is important to that the SAS is returned to the client only using a secure connection via TLS or other secure transport technology.

Disclaimer

I am a software developer first and any security best practices I recommend are just my opinions. If you implement this solution exactly how I have it here, your system will be more secure. That doesn't mean it will be perfect, and hackers won't be able to attack your resources.

Whenever you have security discussions it is important to discuss your risk and security benefits to best determine how much security is needed. 

Getting Started

When working with Azure Blob Storage in the .NET Ecosystem you use the SDK that provides wrappers around all of the HTTP calls which makes it simple to interact with the Storage Account. An upload statement might look like the code snippet below

// Replace with actual connection string
string connection = "CONNECTION_STRING_TO_STORAGE;

// Create client connection
BlobServiceClient client = new BlobServiceClient(connection);

// Create a container reference
BlobContainerClient container = await client.CreateBlobContainerAsync("my-container");

// Create a blob reference
BlobClient blob = contianer.GetBlockBlobReference("file-name");

// Upload stream
await blob.UploadFromStreamAsync(stream);

This is a basic example of uploading files to Blob Storage.

Once you start using SAS as your connection string this code does not change. The only difference is the value of the connection property as it will be a limited access connection string based on rules your code defines.

Strategy

Before we start migrating our existing code to start using SAS we need to go over our strategy to solve this problem. The connection string will still be used by one of our servers running an API and that will provide the SAS token for client side usage.

  1. API that returns SAS Token (Azure Function, Web API, etc.)
  2. Client Side Code (Xamarin App, WPF, UWP, etc.)

Backend API - SAS Token

Let's create a simple HTTP Azure Function that returns a SAS Token when requested. The Azure Function code will communicate directly to your Azure Blob Storage using the connection string. This access will then determine based on the request what type of SAS Token to generate and return it to the sender.

Make sure all of your requests to the Backend API are using TLS (HTTPS) otherwise bad actors may gain access to your SAS Token. One of my best practices is to only make a SAS Token valid for as long as you think it will be used. If the client is only going to make 1 API call maybe, you give them the token for 10 minutes and then they need to request a new one. 

This code sample does not include any security on the Backend Azure Function. To properly implement this solution you should authorize any request.

Create Azure Function

Create a simple HTTP Azure Function using the toolchain of your choice, we are going to use Visual Studio to create it. The generated code should look something like this

public static class SasTokenFunction
{
    [FunctionName("SasTokenFunction")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.Query["name"];

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        return name != null
            ? (ActionResult)new OkObjectResult($"Hello, {name}")
            : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
    }
}

Let's remove most of the code so we have a clean function to work with. I am going to configure the function to return a string value which will eventually be the SAS Token for the client app to use.

public static class SasTokenFunction
{
    [FunctionName("SasTokenFunction")]
    public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req)
    {
        // TODO - populate with actual SAS Token
        string sasUriToken = "";

        return (ActionResult)new OkObjectResult(sasUriToken);
    }
}

Add Connection String

For the Backend to properly generate the SAS Token it will need direct access to the Azure Blob Storage account via the Connection String. Start by opening the local.Settings.json and adding a new row to store your connection string. I called my new setting value AzureStorage

Here is a completed local.Settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "AzureStorage": "ADD_CONNECTION_STRING_HERE"
  }
}

I'm not using a real connection string, so just plug in your connection string to the value.

Reading Your Connection String

There are many ways to read our new AzureStoreage connection string into the function. 

  • Use Dependency Injection
  • Static API calls - Environment.GetEnvironmentVariable
  • Build it manually
  • etc.

I prefer Dependency Injection and if you want to go down that path you really should check out my Azure Functions Dependency Injection Article.

We are going to use a simple static API built right into .NET that will allow us to access the value of AzureStorage. Let's create a new string variable to store the connection string.

string connectionString = Environment.GetEnvironmentVariable("AzureStorage");

 If you attach the debugger or use the logger you will see your connection string being set correctly to the variable connectionString. Now we can move on to connecting to our instance of Azure Blob Storage and generating the SAS Token.

Connect to Azure Blob Storage

It is time to use the connectionString variable and make your connection to Azure Blob Storage. In Azure Functions you can inject containers and maniuplate them very easily, but in our case we need to generate a SAS Token which requires a little bit more code. Let's get started!

Add the NuGets

You will need to add the correct NuGet Package which will allow your Azure Function code to communicate with Blob Storage. This library can be used in any API project and isn't specific to Azure Functions.

  • Microsoft.Azure.Storage.Blob - 11.1.5

You can always use a newer version than 11.1.5 but this is the version that the blog was written against.

Add the Code

Now that you have the necessary NuGet Packages included in your project, you can add the code to connect to the Azure Blob Storage. In code this is known as a CloudStorageAccount, which will use the connectionString we retrieved earlier. Once you have the CloudStorageAccount you will need to make a couple more API calls to retrieve your CloudBlobContainer

// Retrieve the CloudStorageAccount using the connection string
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);

// Create an instance of the CloudBlobClient
CloudBlobClient client = storageAccount.CreateCloudBlobClient();

// Retrieve an instance of the container using our hard-coded container name
CloudBlobContainer container = client.GetContainerReference("my-samples");

Generate SAS Token

The fun part is building the SAS Token! Before we dive right into SAS Token generation you need to ask your code a few questions:

  • Who requesting the token?
  • Do they need Read Only Access?
  • Do they need Write or more elevated admin access?
  • How long do they need the token for?

These are all questions that I can't answer for you and your code needs to answer. A SAS Token is an all access pass and whoever holds the token can use it. These tokens should be treated with the same level of security as an Access Token for authentication purposes. If a bad actor were to sniff a SAS Token they would have all the powers of that token.

If you are using TLS (HTTPS) it is fairly safe to send these token to a client app for usage. I recommend just setting a time limit of 5-30 minutes. At that point the client app can request a new SAS Token.

Your code will need to complete the following steps to generate the SAS Token

  1. Define the SharedAccessBlobPolicy
  2. Generate the SAS Token

Define the Policy

The first step is really answering the questions above, but once you have those questions answered you can define your access policy. In our example we are going to give our client app access to Create and Write. The client app will be able to create a blob for their file and then upload it. 

The code to create the policy

SharedAccessBlobPolicy accessPolicy = new SharedAccessBlobPolicy
{
    // Define expiration to be 30 minutes from now in UTC
    SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(30),

    // Add permissions
    Permissions = SharedAccessBlobPermissions.Create | SharedAccessBlobPermissions.Write
};

Create the SAS Token

With our SharedAccessBlobPolicy in place and our security concerns addressed our code can now generate the full SAS Token that will be used by our client app. 

string token = container.GetSharedAccessSignature(accessPolicy);

The token alone is not very useful if the client app doesn't know the Azure Blob Storage URI. As a best practice I tend to combine the 2 strings to generate a full temporary connection string that a client app could use without question.

To retrieve the URI of the container

string containerUri = container.Uri;

Putting it all together with string interpolation

string sasUriToken = $"{containerUri}{token}";

Return the SAS Token

All that is left is to return the sasUriToken at the end of the Azure Function. Then the client app will able to make a connection. 

return (ActionResult)new OkObjectResult(sasUriToken);

Completed Azure Function

At this point you should have a working Azure Function that generates a SAS Token for a client app to use. Below is our completed code sample

public static class SasTokenFunction
{
    [FunctionName("SasTokenFunction")]
    public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req)
    {
        // Retrieve connection string from settings
        string connectionString = Environment.GetEnvironmentVariable("AzureStorage");

        // Retrieve the CloudStorageAccount using the connection string
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);

        // Create an instance of the CloudBlobClient
        CloudBlobClient client = storageAccount.CreateCloudBlobClient();

        // Retrieve an instance of the container using our hard-coded container name
        CloudBlobContainer container = client.GetContainerReference("my-samples");

        // Define Access Policy
        SharedAccessBlobPolicy accessPolicy = new SharedAccessBlobPolicy
        {
            // Define expiration to be 30 minutes from now in UTC
            SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(30),

            // Add permissions
            Permissions = SharedAccessBlobPermissions.Create | SharedAccessBlobPermissions.Write
        };

        string sasUriToken = $"{container.Uri}{container.GetSharedAccessSignature(accessPolicy)}";

        // return the SAS Token to the client app
        return (ActionResult)new OkObjectResult(sasUriToken);
    }
}

The Client App

The client app can really be any type of app or even service that you want to request a SAS Token prior to making any API calls to the Azure Blob Storage. Update our code sample from before to use the new SAS Token that our Azure Function is returning. Our process stays the same except we need to retrieve a new connection string from our backend

  1. Create HTTP Request to retrieve SAS Token
  2. Update connection string

Request SAS from Backend

Create a simple HTTP Request that retrieves the SAS Token from the Backend Azure Function. If you have added any additional security layers in place such as Bearer Tokens you will need to add those to the requst.

// Define connection string variable
string sasUriToken = string.Empty;

// Add backend API route, this shouldn't be hard-coded
string backendApi = "http://localhost:7071/api/SasTokenFunction";

using (var client = new HttpClient())
{
    // Make request to Backend Azure Function
    HttpResponseMessage response = await client.GetAsync(backendApi);

    // If the request succeeds update the value of sasUriToken to be the response
    if (response.IsSuccessStatusCode)
        sasUriToken = await response.ReadAsStringAsync();
}

Plug in New Connection String

Now that the Backend Azure Function request returned a sasUriToken you can plug that in as if it were a connection string for the CloudBlobContainer.

BlobServiceClient client = new BlobServiceClient(uriSasToken);

The updated code sample from the beginning of the article now completed, looks like this:

// Create client connection
BlobServiceClient client = new BlobServiceClient(uriSasToken);
 
// Create a container reference
BlobContainerClient container = await client.CreateBlobContainerAsync("my-container");
 
// Create a blob reference
BlobClient blob = contianer.GetBlockBlobReference("file-name");
 
// Upload stream
await blob.UploadFromStreamAsync(stream);

Completed Client App Code

Now you have requested the SAS Token from the Backend and used it in the client app here is the completed code:

// Define connection string variable
string sasUriToken = string.Empty;
 
// Add backend API route, this shouldn't be hard-coded
string backendApi = "http://localhost:7071/api/SasTokenFunction";
 
using (var client = new HttpClient())
{
    // Make request to Backend Azure Function
    HttpResponseMessage response = await client.GetAsync(backendApi);
 
    // If the request succeeds update the value of sasUriToken to be the response
    if (response.IsSuccessStatusCode)
        sasUriToken = await response.ReadAsStringAsync();

// Create client connection
BlobServiceClient client = new BlobServiceClient(uriSasToken);
  
// Create a container reference
BlobContainerClient container = await client.CreateBlobContainerAsync("my-container");
  
// Create a blob reference
BlobClient blob = contianer.GetBlockBlobReference("file-name");
  
// Upload stream
await blob.UploadFromStreamAsync(stream);

Conclusion

We introduced the concept of using a Backend Server (Azure Function) to generate a SAS Token URI and allowing a Client App to make API calls to an Azure Blob Storage instance. This article is great to get up and running but doesn't cover anything. By generating SAS Tokens in this manner you open up security concerns that need to be addressed. Here are some security best practices:

  • Add authorization code to the Azure Function to make sure the requesting code is allowed to retrieve a SAS Token
  • Update the Blob Access Policies to expiry the SAS Token after a certain amount of time. I recommend 5-30 minutes as the client app can always request a new SAS Token
  • Always use TLS (HTTPS) to prevent bad actors from sniffing your packets

If you want to see this in action, check out the sample application that I built based off this blog. The Client App is using a Xamarin.Forms project but it can work in many different scenarios outside of mobile

  • Sample Code

-Happy Coding


Share

Tags